diff --git a/.github/dispatch-policyengine-release.sh b/.github/dispatch-policyengine-release.sh index fa2b3579..b41f22e0 100755 --- a/.github/dispatch-policyengine-release.sh +++ b/.github/dispatch-policyengine-release.sh @@ -1,13 +1,19 @@ #!/usr/bin/env bash # # Notify consumer repositories that a policyengine release is available on -# PyPI by sending a policyengine-release repository dispatch, which triggers -# their update workflows to open single-version bump PRs. +# PyPI by sending a repository dispatch, which triggers their update +# workflows. # # Environment: -# VERSION Exact policyengine version that was released. -# GH_TOKEN PolicyEngine GitHub App token with Contents write access to the -# consumer repositories. +# VERSION Exact policyengine version that was released. +# GH_TOKEN PolicyEngine GitHub App token with Contents write access to +# the consumer repositories. +# CONSUMER_REPOS Space-separated repositories to notify (default: +# policyengine-api policyengine-sim-api). +# PAYLOAD_FILE Optional JSON file holding the whole dispatch body +# ({"event_type": ..., "client_payload": {...}}), as written +# by scripts/release_consumers.py. Without it, a +# policyengine-release event carrying only the version is sent. set -euo pipefail if [[ -z "${VERSION:-}" ]]; then @@ -15,14 +21,31 @@ if [[ -z "${VERSION:-}" ]]; then exit 1 fi -CONSUMER_REPOS=( - policyengine-api - policyengine-sim-api +read -r -a REPOS <<< "${CONSUMER_REPOS:-policyengine-api policyengine-sim-api}" + +if [[ -n "${PAYLOAD_FILE:-}" ]]; then + payload_version=$(python - "$PAYLOAD_FILE" <<'PY' +import json +import sys + +payload = json.load(open(sys.argv[1]))["client_payload"] +request = payload.get("request") +print(payload.get("version") or request["candidate"]["policyengine"]["version"]) +PY ) + if [[ "$payload_version" != "$VERSION" ]]; then + echo "ERROR: ${PAYLOAD_FILE} is for ${payload_version}, not ${VERSION}." >&2 + exit 1 + fi +fi -for repo in "${CONSUMER_REPOS[@]}"; do - gh api "repos/PolicyEngine/${repo}/dispatches" \ - -f event_type=policyengine-release \ - -f "client_payload[version]=${VERSION}" - echo "Dispatched policyengine-release ${VERSION} to PolicyEngine/${repo}." +for repo in "${REPOS[@]}"; do + if [[ -n "${PAYLOAD_FILE:-}" ]]; then + gh api "repos/PolicyEngine/${repo}/dispatches" --method POST --input "$PAYLOAD_FILE" + else + gh api "repos/PolicyEngine/${repo}/dispatches" \ + -f event_type=policyengine-release \ + -f "client_payload[version]=${VERSION}" + fi + echo "Dispatched policyengine ${VERSION} to PolicyEngine/${repo}." done diff --git a/.github/qualification-ledger.sh b/.github/qualification-ledger.sh new file mode 100755 index 00000000..5906ce64 --- /dev/null +++ b/.github/qualification-ledger.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +# +# Check out, or publish to, the qualification ledger branch. +# +# .github/qualification-ledger.sh checkout +# .github/qualification-ledger.sh publish "" +# +# The ledger is an orphan branch that only grows. Each attempt writes its own +# directory (uk//attempts//), which concurrent publishes never +# share. A rejected push is retried by replaying the single new commit on the +# latest tip; the new commit may be the branch's first (two runs can both find +# no ledger yet), so it is replayed with cherry-pick, which needs no parent. +# +# Two attempts of one pair do share that pair's published files +# (qualification_receipt.json, latest_attempt.json and pair.json). Git must +# never merge those line by line: a textual merge of two receipts can produce +# a valid-looking receipt that is neither (one attempt's harness with another's +# replay). They are replayed as binary, so a concurrent change to them +# conflicts. When those are the only conflicts and LEDGER_REDERIVE is set, the +# tip's copies are kept and "$LEDGER_REDERIVE " recomputes every pair's +# published files from its immutable attempts; otherwise the publish fails. +set -euo pipefail + +BRANCH="${LEDGER_BRANCH:-uk-qualification-ledger}" +command="${1:?usage: qualification-ledger.sh checkout|publish [message]}" +dir="${2:?ledger directory required}" + +case "$command" in + checkout) + if git ls-remote --exit-code --heads origin "$BRANCH" >/dev/null 2>&1; then + git fetch --quiet origin "$BRANCH" + git worktree add --quiet --force -B "$BRANCH" "$dir" FETCH_HEAD + else + git worktree add --quiet --detach "$dir" + ( + cd "$dir" + git switch --quiet --orphan "$BRANCH" + cat > README.md <<'README' +# UK candidate qualification ledger + +Written by `.github/workflows/certify-uk-release.yaml` in PolicyEngine/policyengine.py. +Each `uk//` directory holds a candidate pair, every qualification +attempt's gate evidence, the replay request and receipt, and the latest +qualification receipt. Nothing is deleted. +README + ) + fi + ;; + publish) + message="${3:?commit message required}" + cd "$dir" + git add -A + if git diff --cached --quiet; then + echo "Ledger unchanged." + exit 0 + fi + identity=(-c user.name="policyengine-qualification[bot]" + -c user.email="qualification@policyengine.org") + git "${identity[@]}" commit --quiet -m "$message" + new_commit="$(git rev-parse HEAD)" + delay="${LEDGER_RETRY_DELAY:-5}" + attributes="$(mktemp)" + trap 'rm -f "$attributes"' EXIT + printf '%s\n' \ + 'qualification_receipt.json merge=binary' \ + 'latest_attempt.json merge=binary' \ + 'pair.json merge=binary' > "$attributes" + for attempt in 1 2 3 4 5 6; do + if git push --quiet origin "HEAD:refs/heads/$BRANCH"; then + echo "Published ledger: $message" + exit 0 + fi + # Someone else published first: replay this one commit on their tip. + if ! git fetch --quiet origin "$BRANCH"; then + sleep $((attempt * delay)) + continue + fi + git reset --quiet --hard FETCH_HEAD + if ! git "${identity[@]}" -c core.attributesFile="$attributes" \ + cherry-pick --allow-empty "$new_commit" >/dev/null 2>&1; then + conflicted="$(git diff --name-only --diff-filter=U)" + shared_only=true + while IFS= read -r path; do + [ -z "$path" ] && continue + case "$path" in + uk/*/qualification_receipt.json|uk/*/latest_attempt.json|uk/*/pair.json) + # Only a pair's published files, never an attempt's own. + case "$path" in uk/*/*/*) shared_only=false ;; esac ;; + *) shared_only=false ;; + esac + done <<< "$conflicted" + if [ -z "$conflicted" ] || [ "$shared_only" != true ] \ + || [ -z "${LEDGER_REDERIVE:-}" ]; then + git cherry-pick --abort || true + echo "::error::The ledger commit conflicts with a concurrent publish." + exit 1 + fi + while IFS= read -r path; do + git checkout --ours -- "$path" + done <<< "$conflicted" + # Recompute published files from the attempts, the tip's and this one's. + $LEDGER_REDERIVE "$PWD" + git add -A + # Completes the cherry-pick, keeping its message, even if empty. + git "${identity[@]}" commit --quiet --allow-empty --no-edit + fi + sleep $((attempt * delay)) + done + echo "::error::Could not publish the qualification ledger after 6 attempts." + exit 1 + ;; + *) + echo "Unknown command: $command" >&2 + exit 2 + ;; +esac diff --git a/.github/workflows/certify-uk-release.yaml b/.github/workflows/certify-uk-release.yaml new file mode 100644 index 00000000..d9bf360b --- /dev/null +++ b/.github/workflows/certify-uk-release.yaml @@ -0,0 +1,501 @@ +# Qualify each new policyengine-uk wheel or uk-data release before promotion. +# +# A candidate pair (newest model wheel, the core it needs, newest data +# release) is resolved and deduplicated against the ledger, certified into a +# candidate commit on candidate/uk/- (manifest, pins, uv.lock, +# TRO, test constants and snapshots regenerated together), qualified by the +# mandatory gates on that exact commit, replayed by policyengine-scorecard, +# and recorded in the uk-qualification-ledger branch. Promotion is +# serialized: it readies the newest promotable candidate PR that advances +# main and closes superseded ones. Merging stays a reviewed step until +# cos d1344 rules on automatic promotion. +# +# Triggers: a policyengine-uk publish or uk-data release (repository +# dispatch), a two-hourly catch-up, or a manual run. A manual run with +# accept_pair_id records a reviewer's acceptance of a needs-review receipt. + +name: Certify UK release candidates + +on: + repository_dispatch: + types: [policyengine-uk-release, policyengine-uk-data-release] + schedule: + - cron: "23 */2 * * *" + workflow_dispatch: + inputs: + model_version: + description: policyengine-uk version (default newest on PyPI) + required: false + data_version: + description: uk-data release (default newest) + required: false + force: + description: Qualify even if the ledger already has a terminal receipt + type: boolean + default: false + accept_pair_id: + description: Accept the needs-review receipt of this pair (reviewer = you) + required: false + accept_receipt_sha256: + description: unreviewed_sha256 of the exact receipt being accepted + required: false + accept_explanation: + description: Why the flagged moves are expected + required: false + +permissions: + contents: write + pull-requests: write + +env: + LEDGER_BRANCH: uk-qualification-ledger + UV_VERSION: "0.12.14" + PYTHON_VERSION: "3.13" + # Keep equal to PYTHON_VERSION. The repository's .python-version asks for a + # newer Python, and every uv command here must use the 3.13 environment the + # job installed. For the same reason each `uv run` passes --no-sync: the + # environment is synchronized explicitly (with the uk and dev extras), and + # a step must run exactly that environment. + UV_PYTHON: "3.13" + # Run by .github/qualification-ledger.sh, inside the ledger worktree, when a + # concurrent publish touched a pair's published files: recompute them from + # the attempts rather than let git merge two receipts. This default uses + # the system Python that `uv pip install --system` provisions; jobs that + # `uv sync` override it with the synchronized environment's interpreter. + LEDGER_REDERIVE: >- + python ${{ github.workspace }}/scripts/check_uk_candidate.py rederive-ledger + +jobs: + resolve: + if: github.event_name != 'workflow_dispatch' || inputs.accept_pair_id == '' + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + actions: read + outputs: + action: ${{ steps.resolve.outputs.action }} + pair_id: ${{ steps.resolve.outputs.pair_id }} + branch: ${{ steps.resolve.outputs.branch }} + base_commit: ${{ steps.resolve.outputs.base_commit }} + attempt_key: ${{ steps.resolve.outputs.attempt_key }} + steps: + - uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + - uses: astral-sh/setup-uv@v8.1.0 + with: + version: ${{ env.UV_VERSION }} + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Install policyengine without country models + run: uv pip install --system -e . + - name: Check out the ledger + run: .github/qualification-ledger.sh checkout ledger + - name: Resolve and deduplicate the candidate + id: resolve + env: + GH_TOKEN: ${{ github.token }} + HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} + EVENT_NAME: ${{ github.event_name }} + EVENT_TYPE: ${{ github.event.action }} + PAYLOAD_VERSION: ${{ github.event.client_payload.version }} + INPUT_MODEL: ${{ inputs.model_version }} + INPUT_DATA: ${{ inputs.data_version }} + INPUT_FORCE: ${{ inputs.force }} + run: | + model="${INPUT_MODEL:-}" + data="${INPUT_DATA:-}" + if [[ "$EVENT_NAME" == "repository_dispatch" && "$EVENT_TYPE" == "policyengine-uk-release" ]]; then + model="$PAYLOAD_VERSION" + elif [[ "$EVENT_NAME" == "repository_dispatch" && "$EVENT_TYPE" == "policyengine-uk-data-release" ]]; then + data="$PAYLOAD_VERSION" + fi + args=(resolve --out candidate.json --ledger ledger) + [[ -n "$model" ]] && args+=(--model-version "$model") + [[ -n "$data" ]] && args+=(--data-version "$data") + [[ "${INPUT_FORCE:-false}" == "true" ]] && args+=(--force) + python scripts/check_uk_candidate.py "${args[@]}" + - uses: actions/upload-artifact@v4 + with: + name: candidate + path: candidate.json + + prepare: + needs: resolve + if: needs.resolve.outputs.action == 'qualify' + runs-on: ubuntu-latest + timeout-minutes: 60 + concurrency: + # Head replacement and readiness must hold the same lock. Otherwise + # promotion can ready a replacement after checking the previous head. + group: promote-uk + cancel-in-progress: false + outputs: + candidate_commit: ${{ steps.stage.outputs.candidate_commit }} + duplicate: ${{ steps.duplicate.outputs.duplicate }} + env: + LEDGER_REDERIVE: >- + ${{ github.workspace }}/.venv/bin/python + ${{ github.workspace }}/scripts/check_uk_candidate.py rederive-ledger + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + - uses: actions/checkout@v6 + with: + ref: ${{ needs.resolve.outputs.base_commit }} + token: ${{ steps.app-token.outputs.token }} + fetch-depth: 0 + # Another run may have staged this attempt while this one waited for + # the promotion concurrency group; never overwrite its evidence. + - name: Check for an attempt staged by another run + id: duplicate + env: + ATTEMPT: ledger/uk/${{ needs.resolve.outputs.pair_id }}/attempts/${{ needs.resolve.outputs.attempt_key }} + run: | + .github/qualification-ledger.sh checkout ledger + if [[ -d "$ATTEMPT" ]]; then + echo "duplicate=true" >> "$GITHUB_OUTPUT" + echo "Attempt already staged by another run; nothing to do." + else + echo "duplicate=false" >> "$GITHUB_OUTPUT" + fi + git worktree remove --force ledger + - uses: actions/download-artifact@v4 + if: steps.duplicate.outputs.duplicate == 'false' + with: + name: candidate + path: ${{ runner.temp }}/candidate + - uses: astral-sh/setup-uv@v8.1.0 + if: steps.duplicate.outputs.duplicate == 'false' + with: + version: ${{ env.UV_VERSION }} + - uses: actions/setup-python@v6 + if: steps.duplicate.outputs.duplicate == 'false' + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Install the base environment + if: steps.duplicate.outputs.duplicate == 'false' + run: uv sync --frozen --extra uk --extra dev --python ${{ env.PYTHON_VERSION }} + - name: Certify the pair and regenerate pins, lock, TROs and constants + if: steps.duplicate.outputs.duplicate == 'false' + env: + HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} + BRANCH: ${{ needs.resolve.outputs.branch }} + run: | + git switch -C "$BRANCH" + uv run --no-sync python scripts/check_uk_candidate.py prepare \ + --candidate "$RUNNER_TEMP/candidate/candidate.json" \ + --out "$RUNNER_TEMP/prepared.json" --skip-snapshots + - name: Install the candidate environment from the regenerated lock + if: steps.duplicate.outputs.duplicate == 'false' + run: uv sync --frozen --extra uk --extra dev --python ${{ env.PYTHON_VERSION }} + - name: Refresh household snapshots on the candidate pair + if: steps.duplicate.outputs.duplicate == 'false' + env: + HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} + PE_UPDATE_SNAPSHOTS: "1" + run: uv run --no-sync pytest tests/test_household_calculator_snapshot.py -q + - name: Check the regenerated bundle + if: steps.duplicate.outputs.duplicate == 'false' + env: + HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} + run: uv run --no-sync python scripts/bundle.py check --include-tros --strict-tros + - name: Commit and push the candidate commit + if: steps.duplicate.outputs.duplicate == 'false' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + BRANCH: ${{ needs.resolve.outputs.branch }} + PAIR_ID: ${{ needs.resolve.outputs.pair_id }} + run: | + # A forced attempt may replace an already qualified head. Revoke + # its ready status before exposing any new, unqualified commit. + ready_pr=$(gh pr list --head "$BRANCH" --state open --json number,isDraft \ + --jq '.[] | select(.isDraft == false) | .number') + if [[ -n "$ready_pr" ]]; then + gh pr ready "$ready_pr" --undo + fi + git add -A + git -c user.name="policyengine-qualification[bot]" \ + -c user.email="qualification@policyengine.org" \ + commit -q -m "Certify UK candidate pair ${PAIR_ID:0:12} for qualification" + git push --force origin "HEAD:refs/heads/$BRANCH" + - name: Stage the attempt in the ledger + id: stage + if: steps.duplicate.outputs.duplicate == 'false' + run: | + .github/qualification-ledger.sh checkout ledger + uv run --no-sync python scripts/check_uk_candidate.py stage \ + --candidate "$RUNNER_TEMP/candidate/candidate.json" --ledger ledger + .github/qualification-ledger.sh publish ledger \ + "Stage UK candidate ${{ needs.resolve.outputs.pair_id }} attempt ${{ needs.resolve.outputs.attempt_key }}" + - name: Open or update the draft candidate PR + if: steps.duplicate.outputs.duplicate == 'false' + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + BRANCH: ${{ needs.resolve.outputs.branch }} + PAIR_ID: ${{ needs.resolve.outputs.pair_id }} + run: | + title="Certify UK candidate ${BRANCH#candidate/uk/}" + body="Automated certification candidate for pair \`$PAIR_ID\`. Qualification is running: https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID} + + Ledger: https://github.com/${GITHUB_REPOSITORY}/tree/${LEDGER_BRANCH}/uk/${PAIR_ID} + + 🤖 Generated with [Claude Code](https://claude.com/claude-code)" + if gh pr view "$BRANCH" --json number >/dev/null 2>&1; then + gh pr edit "$BRANCH" --body "$body" + else + gh pr create --draft --base main --head "$BRANCH" --title "$title" --body "$body" + fi + + qualify: + needs: [resolve, prepare] + if: needs.prepare.outputs.duplicate == 'false' + runs-on: ubuntu-latest + timeout-minutes: 120 + concurrency: + group: uk-candidate-${{ needs.resolve.outputs.pair_id }} + cancel-in-progress: false + env: + LEDGER_REDERIVE: >- + ${{ github.workspace }}/.venv/bin/python + ${{ github.workspace }}/scripts/check_uk_candidate.py rederive-ledger + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ needs.prepare.outputs.candidate_commit }} + fetch-depth: 0 + - uses: astral-sh/setup-uv@v8.1.0 + with: + version: ${{ env.UV_VERSION }} + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Install the candidate environment + run: uv sync --frozen --extra uk --extra dev --python ${{ env.PYTHON_VERSION }} + - name: Check out the ledger + run: .github/qualification-ledger.sh checkout ledger + - name: Run the mandatory local gates + env: + HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} + ATTEMPT: ledger/uk/${{ needs.resolve.outputs.pair_id }}/attempts/${{ needs.resolve.outputs.attempt_key }} + run: uv run --no-sync python scripts/check_uk_candidate.py gates --attempt "$ATTEMPT" --work-dir "$RUNNER_TEMP/qualification" + - name: Generate a GitHub App token for the scorecard + id: scorecard-token + continue-on-error: true + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + owner: PolicyEngine + repositories: policyengine-scorecard + - name: Send the pre-promotion replay request + if: steps.scorecard-token.outcome == 'success' + continue-on-error: true + env: + GH_TOKEN: ${{ steps.scorecard-token.outputs.token }} + ATTEMPT: ledger/uk/${{ needs.resolve.outputs.pair_id }}/attempts/${{ needs.resolve.outputs.attempt_key }} + run: uv run --no-sync python scripts/check_uk_candidate.py request-replay --attempt "$ATTEMPT" --dispatch + - name: Publish the gate evidence + if: always() + run: | + .github/qualification-ledger.sh publish ledger \ + "Gates for UK candidate ${{ needs.resolve.outputs.pair_id }} attempt ${{ needs.resolve.outputs.attempt_key }}" + + replay: + needs: [resolve, prepare, qualify] + if: always() && needs.prepare.result == 'success' && needs.prepare.outputs.duplicate == 'false' + runs-on: ubuntu-latest + timeout-minutes: 355 + permissions: + contents: write + pull-requests: write + actions: write + steps: + - uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + - uses: astral-sh/setup-uv@v8.1.0 + with: + version: ${{ env.UV_VERSION }} + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Install policyengine without country models + run: uv pip install --system -e . + - name: Check out the ledger + run: .github/qualification-ledger.sh checkout ledger + - name: Wait for the replay receipt + env: + GH_TOKEN: ${{ github.token }} + ATTEMPT: ledger/uk/${{ needs.resolve.outputs.pair_id }}/attempts/${{ needs.resolve.outputs.attempt_key }} + run: | + if [[ -f "$ATTEMPT/replay/request.json" ]]; then + python scripts/check_uk_candidate.py poll-replay --attempt "$ATTEMPT" --wait-minutes 340 + else + echo "No replay request was sent; the replay gate stays unavailable." + fi + - name: Finalize and publish the qualification receipt + id: finalize + env: + ATTEMPT: ledger/uk/${{ needs.resolve.outputs.pair_id }}/attempts/${{ needs.resolve.outputs.attempt_key }} + run: | + python scripts/check_uk_candidate.py finalize --attempt "$ATTEMPT" --ledger ledger --pr-body "$RUNNER_TEMP/pr-body.md" + .github/qualification-ledger.sh publish ledger \ + "Qualification receipt for UK candidate ${{ needs.resolve.outputs.pair_id }}" + - name: Refresh failed candidate PR checks against the published receipt + env: + GH_TOKEN: ${{ github.token }} + run: python scripts/check_uk_candidate.py refresh-pr-checks --ledger ledger + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + - name: Update the candidate PR with the verdict + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + BRANCH: ${{ needs.resolve.outputs.branch }} + run: gh pr edit "$BRANCH" --body-file "$RUNNER_TEMP/pr-body.md" + + catch-up: + if: github.event_name == 'schedule' + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + pull-requests: write + actions: write + steps: + - uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + - uses: astral-sh/setup-uv@v8.1.0 + with: + version: ${{ env.UV_VERSION }} + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Install policyengine without country models + run: uv pip install --system -e . + - name: Restart abandoned attempts without replay requests + env: + GH_TOKEN: ${{ github.token }} + run: | + .github/qualification-ledger.sh checkout ledger + python scripts/check_uk_candidate.py recoverable --ledger ledger > recoverable.jsonl + python - <<'PY' + import json + import os + import subprocess + from pathlib import Path + + for line in Path("recoverable.jsonl").read_text().splitlines(): + pair = json.loads(line)["pair"] + subprocess.run( + [ + "gh", "workflow", "run", "certify-uk-release.yaml", + "--repo", os.environ["GITHUB_REPOSITORY"], "--ref", "main", + "-f", f"model_version={pair['model']['version']}", + "-f", f"data_version={pair['data']['release_version']}", + "-f", "force=false", + ], + check=True, + ) + PY + - name: Finish attempts whose replay outlasted their run + env: + GH_TOKEN: ${{ github.token }} + run: | + python scripts/check_uk_candidate.py waiting --ledger ledger > waiting.txt + while read -r attempt; do + [[ -z "$attempt" ]] && continue + python scripts/check_uk_candidate.py poll-replay --attempt "$attempt" + python scripts/check_uk_candidate.py finalize --attempt "$attempt" --ledger ledger + done < waiting.txt + .github/qualification-ledger.sh publish ledger "Catch up waiting UK replay receipts" + - name: Refresh failed candidate PR checks against the published receipts + env: + GH_TOKEN: ${{ github.token }} + run: python scripts/check_uk_candidate.py refresh-pr-checks --ledger ledger + + accept: + if: github.event_name == 'workflow_dispatch' && inputs.accept_pair_id != '' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: write + pull-requests: write + actions: write + steps: + - uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + - uses: astral-sh/setup-uv@v8.1.0 + with: + version: ${{ env.UV_VERSION }} + - name: Install policyengine without country models + run: uv pip install --system -e . + - name: Record the reviewer's acceptance + env: + PAIR_ID: ${{ inputs.accept_pair_id }} + RECEIPT: ${{ inputs.accept_receipt_sha256 }} + EXPLANATION: ${{ inputs.accept_explanation }} + REVIEWER: ${{ github.actor }} + run: | + .github/qualification-ledger.sh checkout ledger + python scripts/check_uk_candidate.py accept --ledger ledger \ + --pair-id "$PAIR_ID" --receipt-sha256 "$RECEIPT" \ + --explanation "$EXPLANATION" --reviewer "$REVIEWER" + .github/qualification-ledger.sh publish ledger "Accept UK candidate $PAIR_ID on review by $REVIEWER" + - name: Refresh failed candidate PR checks against the accepted receipt + env: + GH_TOKEN: ${{ github.token }} + run: python scripts/check_uk_candidate.py refresh-pr-checks --ledger ledger + + promote: + needs: [resolve, replay, catch-up, accept] + if: always() + runs-on: ubuntu-latest + timeout-minutes: 15 + concurrency: + group: promote-uk + cancel-in-progress: false + steps: + - uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + - uses: astral-sh/setup-uv@v8.1.0 + with: + version: ${{ env.UV_VERSION }} + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: Install policyengine without country models + run: uv pip install --system -e . + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + - name: Promote the newest qualified pair that advances main + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + .github/qualification-ledger.sh checkout ledger + python scripts/check_uk_candidate.py promote --ledger ledger --apply diff --git a/.github/workflows/push.yaml b/.github/workflows/push.yaml index c3a9b4a1..ac4cabc8 100644 --- a/.github/workflows/push.yaml +++ b/.github/workflows/push.yaml @@ -7,8 +7,11 @@ # Phase 2 (sentinel commit): Publish to PyPI + GitHub Release # Skips Lint + Test since the code is identical to Phase 1. # After Publish, NotifyConsumers waits for the release to be visible on -# PyPI and sends a policyengine-release repository dispatch to consumer -# repos so they open version-bump PRs. +# PyPI and sends a policyengine-release repository dispatch (version plus +# the certified UK bundle identity and qualification digests) to consumer +# repos so they open version-bump PRs, then a released-bundle replay +# request to policyengine-scorecard. New consumers are dispatched in +# separate, non-blocking steps. name: Push to main @@ -249,6 +252,19 @@ jobs: run: bash .github/wait-for-pypi.sh - name: Validate published SPM bundle before consumer updates run: python -S scripts/bundle.py check --published-spm + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 + with: + version: "0.12.14" + - name: Install policyengine without country models + run: uv pip install -e . --system + # The payload carries the certified UK bundle identity and, for an + # automatically qualified pair, the qualification receipt digest. + # client_payload.version stays first, as existing updaters read only it. + - name: Build the release dispatch payload + env: + VERSION: ${{ needs.Publish.outputs.version }} + run: python scripts/release_consumers.py payload --version "$VERSION" > "$RUNNER_TEMP/release-payload.json" - name: Generate GitHub App token id: app-token uses: actions/create-github-app-token@v1 @@ -261,4 +277,58 @@ jobs: env: GH_TOKEN: ${{ steps.app-token.outputs.token }} VERSION: ${{ needs.Publish.outputs.version }} + PAYLOAD_FILE: ${{ runner.temp }}/release-payload.json + run: bash .github/dispatch-policyengine-release.sh + # New consumers are dispatched separately, so a missing App + # installation on one of them can never block the consumers above. + - name: Generate GitHub App token for the Budget dashboard + id: dashboard-token + continue-on-error: true + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + owner: PolicyEngine + repositories: uk-autumn-budget-dashboard-2026 + - name: Dispatch policyengine-release to the Budget dashboard + if: steps.dashboard-token.outcome == 'success' + continue-on-error: true + env: + GH_TOKEN: ${{ steps.dashboard-token.outputs.token }} + VERSION: ${{ needs.Publish.outputs.version }} + PAYLOAD_FILE: ${{ runner.temp }}/release-payload.json + CONSUMER_REPOS: uk-autumn-budget-dashboard-2026 + run: bash .github/dispatch-policyengine-release.sh + - name: Install the released bundle for the replay request + continue-on-error: true + id: released + env: + VERSION: ${{ needs.Publish.outputs.version }} + run: uv pip install --system "policyengine[uk]==$VERSION" + - name: Build the scorecard's released-bundle replay request + if: steps.released.outcome == 'success' + id: scorecard-request + continue-on-error: true + env: + VERSION: ${{ needs.Publish.outputs.version }} + HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }} + run: python scripts/release_consumers.py scorecard-request --version "$VERSION" > "$RUNNER_TEMP/scorecard-request.json" + - name: Generate GitHub App token for the scorecard + if: steps.scorecard-request.outcome == 'success' + id: scorecard-token + continue-on-error: true + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + owner: PolicyEngine + repositories: policyengine-scorecard + - name: Send the scorecard a released-bundle replay request + if: steps.scorecard-token.outcome == 'success' + continue-on-error: true + env: + GH_TOKEN: ${{ steps.scorecard-token.outputs.token }} + VERSION: ${{ needs.Publish.outputs.version }} + PAYLOAD_FILE: ${{ runner.temp }}/scorecard-request.json + CONSUMER_REPOS: policyengine-scorecard run: bash .github/dispatch-policyengine-release.sh diff --git a/changelog.d/573.added.md b/changelog.d/573.added.md new file mode 100644 index 00000000..6d3d75c3 --- /dev/null +++ b/changelog.d/573.added.md @@ -0,0 +1 @@ +Qualify each new policyengine-uk wheel or uk-data release automatically before promotion. The new `certify-uk-release` workflow and `scripts/check_uk_candidate.py` resolve and deduplicate the candidate pair. They build a candidate commit with the manifest, pins, lock, TRO and test constants regenerated together, and run mandatory gates on it, none of which can be skipped: installed versions, wheel hashes, core compatibility, data access, dataset bytes, supported years, Budget inputs, the reform contract and metric review bounds. The metric review reads the aggregate-impact tool's small-cell format: a figure from fewer than 10 records is never compared, a change that would come from one goes to review, totals rebuilt without different suppressed figures are lined up from published figures where they are sums, a skipped check counts as not run, and only reports in that format are written to the ledger. From the reform contract's population checks, which run on the survey data, the ledger publishes each row's fixed fields (check, route, status and the like) and only the top-level true/false findings of its detail; `docs/engineering/runbooks/uk-qualification.md` explains how to see a failed check's figures locally. They also send policyengine-scorecard an immutable replay request, record a qualification receipt in the `uk-qualification-ledger` branch, and promote in order. Certification gains an exact `qualified_exact_pair` basis backed by the ledger receipt, together with a core compatibility check. The release dispatch now carries the certified UK bundle identity and goes to the Budget dashboard and the scorecard as well. diff --git a/docs/engineering/runbooks/uk-qualification.md b/docs/engineering/runbooks/uk-qualification.md new file mode 100644 index 00000000..89306acb --- /dev/null +++ b/docs/engineering/runbooks/uk-qualification.md @@ -0,0 +1,62 @@ +# UK candidate qualification: what the ledger shows, and reproducing a gate + +The `certify-uk-release` workflow qualifies each new policyengine-uk wheel +or uk-data release before promotion (`scripts/check_uk_candidate.py`, +`policyengine.provenance.uk_candidate`). Each attempt's gate results and +evidence are written to the `uk-qualification-ledger` branch, which is +public, so the evidence keeps nothing from the survey data that could +identify a record. + +## What the ledger withholds + +- **Reform contract, population checks.** These run on the survey data. Each + row's check, route, status and the runner's fixed fields + (`POPULATION_ROW_FIELDS`) are published; from its `detail`, only top-level + `True`/`False`/`None` findings are kept, and every count, total, amount, + text, list or nested mapping reads `"withheld"`. The digest the gate + records is of that published copy, never of the full report. +- **Metrics review and responsiveness.** The aggregate-impact tool publishes + a figure from fewer than 10 records as suppressed, and the gate records no + value from a report that breaks that format; such a report is published + only as its digest and problems. + +## Reproducing a failed population check + +The withheld figures exist only in the runner's report, which is not saved. +To see them, rerun the contract on a machine with access to the private UK +dataset, and keep the output local: it holds figures from the survey data, +some from fewer than 10 records, so never commit, paste or publish it. + +1. Check out the candidate commit the attempt ran on (`git_commit` in the + attempt's `harness.json` in the ledger): its bundle manifest names the + dataset `materialize_dataset` fetches, so an older checkout would fetch + older data even with the candidate's wheels installed. +2. Install the candidate pair (the attempt's `candidate.json` names the + wheels) and export `HUGGING_FACE_TOKEN` for the private dataset. +3. Materialize the certified dataset, check its SHA-256 against + `data.dataset_sha256` in the pair's `pair.json`, and run the contract + with the seed and sample size in + `src/policyengine/data/qualification/uk.json`: + + ```python + import hashlib + import json + from pathlib import Path + + from policyengine.provenance.dataset_materialization import materialize_dataset + from policyengine.tax_benefit_models.uk.budget_reform_contract import ( + run_budget_reform_contract, + ) + + dataset = materialize_dataset("uk", data_dir=Path("data")).path + pair = json.loads(Path("path/to/ledger/uk//pair.json").read_text()) + digest = hashlib.sha256(Path(dataset).read_bytes()).hexdigest() + assert digest == pair["data"]["dataset_sha256"], "not the certified dataset" + report = run_budget_reform_contract( + routes=None, dataset_path=str(dataset), seed=0, max_examples=20 + ) + failing = [row for row in report["population"] if row["status"] != "pass"] + ``` + +4. Read `failing[...]["detail"]` locally. Report only the finding (which + condition failed, on which route), never the figures. diff --git a/scripts/bundle.py b/scripts/bundle.py index 121ab8e2..072b2cb3 100644 --- a/scripts/bundle.py +++ b/scripts/bundle.py @@ -50,6 +50,10 @@ def _certify_data(args: argparse.Namespace) -> int: argv.append("--no-changelog") if args.skip_artifact_check: argv.append("--skip-artifact-check") + if args.core_version: + argv.extend(["--core-version", args.core_version]) + if args.qualified_pair_id: + argv.extend(["--qualified-pair-id", args.qualified_pair_id]) return certify_data_release_main(argv) @@ -226,6 +230,14 @@ def _parser() -> argparse.ArgumentParser: "vendored/regional artifacts." ), ) + certify.add_argument( + "--core-version", + help="policyengine-core version to check against core claims and pin.", + ) + certify.add_argument( + "--qualified-pair-id", + help="Admit the exact pair on automated qualification (needs --core-version).", + ) certify.set_defaults(func=_certify_data) packages = subparsers.add_parser( diff --git a/scripts/certify_data_release.py b/scripts/certify_data_release.py index 8842adbb..7a55ca2f 100644 --- a/scripts/certify_data_release.py +++ b/scripts/certify_data_release.py @@ -72,6 +72,22 @@ def main(argv=None) -> int: default=None, help="Model package version to certify for (default: installed).", ) + parser.add_argument( + "--core-version", + default=None, + help=( + "policyengine-core version to check against the release's core " + "claims and pin in the bundle." + ), + ) + parser.add_argument( + "--qualified-pair-id", + default=None, + help=( + "Admit the exact model, core and data release on automated " + "qualification of this pair (requires --core-version)." + ), + ) parser.add_argument("--no-generate", action="store_true") parser.add_argument("--no-changelog", action="store_true") parser.add_argument( @@ -102,6 +118,8 @@ def main(argv=None) -> int: regional_manifest_uri=args.regional_manifest_uri, regional_artifact_prefix=args.regional_artifact_prefix, regional_path_template=args.regional_path_template, + core_version=args.core_version, + qualified_pair_id=args.qualified_pair_id, ) print(result.summary()) diff --git a/scripts/check_uk_candidate.py b/scripts/check_uk_candidate.py new file mode 100644 index 00000000..e8824681 --- /dev/null +++ b/scripts/check_uk_candidate.py @@ -0,0 +1,750 @@ +"""Qualify a UK model/data candidate before it can be promoted. + +Run by ``.github/workflows/certify-uk-release.yaml``; every step also runs +locally. Network access: PyPI, Hugging Face (``HUGGING_FACE_TOKEN``) and, for +the replay and promotion steps, GitHub (``GH_TOKEN``). + +Each attempt lives in one ledger directory, ``/uk//attempts/ +/``, which every step after ``resolve`` reads and writes:: + + python scripts/check_uk_candidate.py resolve --out candidate.json \\ + [--model-version 2.126.8] [--data-version 1.58.0] [--ledger ledger/] [--force] + python scripts/check_uk_candidate.py prepare --candidate candidate.json --skip-snapshots + python scripts/check_uk_candidate.py stage --candidate candidate.json --ledger ledger/ + python scripts/check_uk_candidate.py gates --attempt + python scripts/check_uk_candidate.py request-replay --attempt [--dispatch] + python scripts/check_uk_candidate.py poll-replay --attempt [--wait-minutes 340] + python scripts/check_uk_candidate.py finalize --attempt --ledger ledger/ + python scripts/check_uk_candidate.py waiting --ledger ledger/ + python scripts/check_uk_candidate.py refresh-pr-checks --ledger ledger/ + python scripts/check_uk_candidate.py promote --ledger ledger/ [--apply] + python scripts/check_uk_candidate.py accept --ledger ledger/ --pair-id ... \\ + --receipt-sha256 ... --explanation "..." [--reviewer login] + +Steps that the workflow branches on also write ``key=value`` lines to +``$GITHUB_OUTPUT`` when it is set. +""" + +from __future__ import annotations + +import argparse +import datetime +import json +import os +import subprocess +import sys +import time +from functools import lru_cache +from pathlib import Path +from typing import Any, Optional + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT / "src")) +os.environ.setdefault("POLICYENGINE_SKIP_COUNTRY_IMPORTS", "1") + +import requests # noqa: E402 + +from policyengine.provenance.qualification import ( # noqa: E402 + CandidatePair, + IssuedBy, + QualificationHarness, + ReplayQualificationRequest, +) +from policyengine.provenance.qualification_ledger import ( # noqa: E402 + LEDGER_BRANCH, + LEDGER_REPOSITORY, + LedgerInconsistency, + attempt_directory, + latest_receipts, + pair_directory, + published_receipt, + read_json, + rederive_publication, + write_json, +) +from policyengine.provenance.uk_qualification import ( # noqa: E402 + ResolvedCandidate, + WheelFile, + accept_review, + attempt_is_recoverable, + attempt_is_waiting_for_replay, + attempt_key, + attempt_supersedes_receipt, + build_replay_request, + candidate_action, + candidate_branch, + candidate_pr_body, + expected_release_bundle, + finalize_attempt, + packaged_manifest_sha256, + plan_promotion, + prepare_candidate_commit, + previous_certificate, + read_replay, + replay_dispatch_payload, + resolve_candidate, + stage_attempt, + write_gate_results, +) + +BUNDLE_PATH = REPO_ROOT / "src" / "policyengine" / "data" / "bundle" / "manifest.json" +CONFIG_PATH = REPO_ROOT / "src" / "policyengine" / "data" / "qualification" / "uk.json" +TIMEOUT = 60 + + +def utc_now() -> str: + return datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%SZ") + + +def config() -> dict[str, Any]: + return json.loads(CONFIG_PATH.read_text()) + + +def hf_headers() -> dict[str, str]: + token = os.environ.get("HUGGING_FACE_TOKEN") or os.environ.get("HF_TOKEN") + return {"Authorization": f"Bearer {token}"} if token else {} + + +def pypi_project(path: str) -> dict[str, Any]: + response = requests.get(f"https://pypi.org/pypi/{path}/json", timeout=TIMEOUT) + response.raise_for_status() + return response.json() + + +def hf_revision_sha(repo_id: str, revision: str) -> str: + response = requests.get( + f"https://huggingface.co/api/models/{repo_id}/revision/{revision}", + headers=hf_headers(), + timeout=TIMEOUT, + ) + response.raise_for_status() + return str(response.json()["sha"]) + + +def hf_tags(repo_id: str) -> list[str]: + response = requests.get( + f"https://huggingface.co/api/models/{repo_id}/refs", + headers=hf_headers(), + timeout=TIMEOUT, + ) + response.raise_for_status() + return [tag["name"] for tag in response.json().get("tags", [])] + + +def hf_file(repo_id: str, revision: str, path: str) -> bytes: + response = requests.get( + f"https://huggingface.co/{repo_id}/resolve/{revision}/{path}", + headers=hf_headers(), + timeout=TIMEOUT, + ) + response.raise_for_status() + return response.content + + +def scorecard_file(repository: str): + token = os.environ.get("GH_TOKEN") or os.environ.get("GITHUB_TOKEN") + headers = {"Accept": "application/vnd.github.raw+json"} + if token: + headers["Authorization"] = f"Bearer {token}" + + def fetch(ref: str, path: str) -> Optional[bytes]: + response = requests.get( + f"https://api.github.com/repos/{repository}/contents/{path}", + params={"ref": ref}, + headers=headers, + timeout=TIMEOUT, + ) + if response.status_code == 404: + return None + response.raise_for_status() + return response.content + + return fetch + + +def issued_by() -> IssuedBy: + server = os.environ.get("GITHUB_SERVER_URL", "https://github.com") + repository = os.environ.get("GITHUB_REPOSITORY", LEDGER_REPOSITORY) + run_id = int(os.environ.get("GITHUB_RUN_ID", "1")) + return IssuedBy( + repository=repository, + workflow_run_id=run_id, + run_attempt=int(os.environ.get("GITHUB_RUN_ATTEMPT", "1")), + run_url=f"{server}/{repository}/actions/runs/{run_id}", + ) + + +def git(*args: str, cwd: Path = REPO_ROOT) -> str: + return subprocess.run( + ["git", *args], cwd=cwd, check=True, capture_output=True, text=True + ).stdout.strip() + + +def set_output(**values: str) -> None: + path = os.environ.get("GITHUB_OUTPUT") + if not path: + return + with open(path, "a") as stream: + for key, value in values.items(): + stream.write(f"{key}={value}\n") + + +def resolved_from(payload: dict[str, Any]) -> ResolvedCandidate: + return ResolvedCandidate( + pair=CandidatePair.model_validate(payload["pair"]), + model_wheel=WheelFile(**payload["model_wheel"]), + core_wheel=WheelFile(**payload["core_wheel"]), + manifest_uri=payload["manifest_uri"], + model_requires_core=payload["model_requires_core"], + ) + + +def ledger_url(pair_id: str) -> str: + return f"https://github.com/{LEDGER_REPOSITORY}/tree/{LEDGER_BRANCH}/uk/{pair_id}" + + +# --- Commands ----------------------------------------------------------------- + + +def cmd_resolve(args: argparse.Namespace) -> int: + bundle = json.loads(BUNDLE_PATH.read_text()) + resolved = resolve_candidate( + current_core=bundle["packages"]["policyengine-core"]["version"], + pypi_project=pypi_project, + hf_revision_sha=hf_revision_sha, + hf_tags=hf_tags, + hf_file=hf_file, + model_version=args.model_version or None, + data_version=args.data_version or None, + ) + base_commit = git("rev-parse", "HEAD") + resolved_at = utc_now() + action, reason = candidate_action( + resolved.pair, + bundle, + Path(args.ledger) if args.ledger else None, + base_commit=base_commit, + force=args.force, + now=resolved_at, + owner_is_failed=owner_run_failed, + ) + key = attempt_key( + resolved.pair, + Path(args.ledger) if args.ledger else None, + base_commit=base_commit, + force=args.force, + now=resolved_at, + owner_is_failed=owner_run_failed, + ) + payload = { + "pair": resolved.pair.canonical_payload(), + "pair_id": resolved.pair.pair_id, + "model_wheel": resolved.model_wheel.__dict__, + "core_wheel": resolved.core_wheel.__dict__, + "manifest_uri": resolved.manifest_uri, + "model_requires_core": resolved.model_requires_core, + "base_commit": base_commit, + "attempt_key": key, + "branch": candidate_branch(resolved.pair), + "action": action, + "reason": reason, + "previous_certificate": previous_certificate(bundle).canonical_payload(), + } + if action == "qualify" and args.ledger: + pair_dir = pair_directory(Path(args.ledger), "uk", resolved.pair.pair_id) + if (pair_dir / "qualification_receipt.json").exists(): + receipt = published_receipt(pair_dir) + if receipt.verdict != "incomplete": + # Forced work, or recovery of work already bound to this receipt. + payload["supersedes_receipt_sha256"] = receipt.unreviewed_sha256 + write_json(Path(args.out), payload) + print( + f"{action}: {reason} (pair {resolved.pair.pair_id[:12]}, {resolved.pair.order_key})" + ) + set_output( + action=action, + pair_id=resolved.pair.pair_id, + branch=candidate_branch(resolved.pair), + base_commit=base_commit, + attempt_key=key, + ) + return 0 + + +def cmd_prepare(args: argparse.Namespace) -> int: + payload = json.loads(Path(args.candidate).read_text()) + prepared = prepare_candidate_commit( + REPO_ROOT, + resolved_from(payload), + python=sys.executable, + refresh_snapshots=not args.skip_snapshots, + ) + write_json(Path(args.out), prepared) + print(json.dumps(prepared, indent=2)) + return 0 + + +def cmd_stage(args: argparse.Namespace) -> int: + from importlib.metadata import version + + payload = json.loads(Path(args.candidate).read_text()) + harness = QualificationHarness( + policyengine_version=version("policyengine"), + git_commit=git("rev-parse", "HEAD"), + packaged_bundle_manifest_sha256=packaged_manifest_sha256(REPO_ROOT), + ) + attempt = attempt_directory( + Path(args.ledger), "uk", payload["pair_id"], payload["attempt_key"] + ) + stage_attempt( + attempt, + payload, + harness, + owner_run_id=os.environ.get("GITHUB_RUN_ID"), + staged_at=utc_now(), + ) + print(f"staged {attempt} at {harness.git_commit}") + set_output(attempt=str(attempt), candidate_commit=harness.git_commit) + return 0 + + +def _attempt( + args: argparse.Namespace, +) -> tuple[Path, dict[str, Any], QualificationHarness]: + attempt = Path(args.attempt) + payload = read_json(attempt / "candidate.json") + harness = QualificationHarness.model_validate(read_json(attempt / "harness.json")) + return attempt, payload, harness + + +def cmd_gates(args: argparse.Namespace) -> int: + from policyengine.provenance import uk_candidate + + attempt, payload, harness = _attempt(args) + if git("rev-parse", "HEAD") != harness.git_commit: + raise SystemExit( + "Gates must run on the candidate commit named in harness.json." + ) + ledger_root = attempt.parents[3] + results, evidence = uk_candidate.run_candidate_gates( + resolved_from(payload), + harness, + config=config(), + work_dir=Path(args.work_dir), + previous_certificate=payload.get("previous_certificate"), + ledger_root=ledger_root, + ) + write_gate_results(attempt, results, evidence) + for result in results: + print(f"{result.gate_id:20s} {result.status:13s} {result.summary}") + return 0 + + +def cmd_request_replay(args: argparse.Namespace) -> int: + attempt, payload, harness = _attempt(args) + request = build_replay_request( + CandidatePair.model_validate(payload["pair"]), + harness, + release_bundle=expected_release_bundle(sys.executable, cwd=REPO_ROOT), + issued_by=issued_by(), + attempt=args.replay_attempt, + compare_to=config()["replay"]["compare_to"], + ) + print(f"request_id {request.request_id}") + if args.dispatch: + settings = config()["replay"] + subprocess.run( + [ + "gh", + "api", + f"repos/{settings['repository']}/dispatches", + "--method", + "POST", + "--input", + "-", + ], + input=json.dumps( + replay_dispatch_payload(request, event_type=settings["event_type"]) + ), + text=True, + check=True, + ) + print(f"dispatched {settings['event_type']} to {settings['repository']}") + # Recorded only once sent: an attempt with a request waits for its replay. + write_json(attempt / "replay" / "request.json", request.document()) + set_output(request_id=request.request_id) + return 0 + + +def cmd_poll_replay(args: argparse.Namespace) -> int: + attempt = Path(args.attempt) + settings = config()["replay"] + request = ReplayQualificationRequest.model_validate( + read_json(attempt / "replay" / "request.json") + ) + fetch = scorecard_file(settings["repository"]) + deadline = time.monotonic() + 60 * args.wait_minutes + while True: + gate, status, receipt = read_replay( + request, fetch=fetch, state_ref=settings["state_ref"] + ) + if (status is not None and status.terminal) or time.monotonic() >= deadline: + break + time.sleep(args.interval_seconds) + write_json(attempt / "replay" / "replay.result.json", gate.canonical_payload()) + if status is not None: + write_json(attempt / "replay" / "status.json", status.document()) + if receipt is not None: + write_json(attempt / "replay" / "receipt.json", receipt.document()) + terminal = status is not None and status.terminal + print(f"replay {gate.status}: {gate.summary}") + set_output(replay_terminal="true" if terminal else "false") + return 0 + + +def cmd_finalize(args: argparse.Namespace) -> int: + attempt = Path(args.attempt) + receipt = finalize_attempt( + attempt, Path(args.ledger), issued_by=issued_by(), completed_at=utc_now() + ) + payload = read_json(attempt / "candidate.json") + body = candidate_pr_body( + receipt, receipt.pair, ledger_url=ledger_url(receipt.pair_id) + ) + if args.pr_body: + Path(args.pr_body).write_text(body) + print(f"verdict {receipt.verdict}; receipt {receipt.receipt_sha256}") + set_output( + verdict=receipt.verdict, + receipt_sha256=receipt.receipt_sha256, + branch=payload["branch"], + ) + return 0 + + +def cmd_waiting(args: argparse.Namespace) -> int: + base = Path(args.ledger) / "uk" + waiting = sorted( + str(path) + for path in base.glob("*/attempts/*") + if path.is_dir() and attempt_is_waiting_for_replay(path) + ) + for path in waiting: + print(path) + set_output(waiting=json.dumps(waiting)) + return 0 + + +@lru_cache(maxsize=256) +def owner_run_failed(run_id: str) -> bool: + """Only a confirmed failed terminal run relinquishes its unexpired stage.""" + + result = subprocess.run( + [ + "gh", + "run", + "view", + run_id, + "--repo", + LEDGER_REPOSITORY, + "--json", + "status,conclusion", + ], + text=True, + capture_output=True, + check=False, + ) + if result.returncode: + return False + try: + state = json.loads(result.stdout) + except json.JSONDecodeError: + return False + return state.get("status") == "completed" and state.get("conclusion") in { + "failure", + "cancelled", + "timed_out", + "startup_failure", + "action_required", + "stale", + } + + +def cmd_recoverable(args: argparse.Namespace) -> int: + now = utc_now() + for pair_dir in sorted((Path(args.ledger) / "uk").glob("*")): + receipt = pair_dir / "qualification_receipt.json" + stages = list((pair_dir / "attempts").glob("*/staging.json")) + if not stages: + continue + attempt = max(stages, key=lambda path: int(read_json(path)["sequence"])).parent + if receipt.exists(): + try: + terminal = published_receipt(pair_dir) + except LedgerInconsistency as error: + print(f"::warning::{error}", file=sys.stderr) + continue + if terminal.verdict != "incomplete" and not attempt_supersedes_receipt( + attempt, terminal + ): + continue + if attempt_is_recoverable(attempt, now=now, owner_is_failed=owner_run_failed): + print(json.dumps(read_json(attempt / "candidate.json"))) + return 0 + + +def _open_candidate_branches() -> dict[str, str]: + """Open candidate branches and the commit each one's pull request is at.""" + + listing = subprocess.run( + [ + "gh", + "pr", + "list", + "--repo", + LEDGER_REPOSITORY, + "--state", + "open", + "--search", + "head:candidate/uk/", + "--json", + "headRefName,headRefOid", + ], + check=True, + capture_output=True, + text=True, + ).stdout + return {item["headRefName"]: item["headRefOid"] for item in json.loads(listing)} + + +def cmd_refresh_pr_checks(args: argparse.Namespace) -> int: + """Re-evaluate failed PR checks now that their exact receipt is published. + + The PR workflow does not subscribe to edits or ready_for_review events. + GITHUB_TOKEN may re-run existing failed runs with actions: write. + """ + + branches = _open_candidate_branches() + for receipt in latest_receipts(Path(args.ledger)): + branch = candidate_branch(receipt.pair) + head = receipt.harness.git_commit + if not receipt.promotable or branches.get(branch) != head: + continue + listing = subprocess.run( + [ + "gh", + "api", + f"repos/{LEDGER_REPOSITORY}/actions/runs" + f"?head_sha={head}&event=pull_request&per_page=100", + "--paginate", + "--jq", + ".workflow_runs[] | {id, head_sha, event, status, conclusion} | tojson", + ], + check=True, + capture_output=True, + text=True, + ).stdout + for line in listing.splitlines(): + run = json.loads(line) + if ( + run["head_sha"] != head + or run["event"] != "pull_request" + or run["status"] != "completed" + or run["conclusion"] not in {"failure", "timed_out"} + ): + continue + subprocess.run( + [ + "gh", + "run", + "rerun", + str(run["id"]), + "--repo", + LEDGER_REPOSITORY, + "--failed", + ], + check=True, + ) + print(f"Re-running failed PR checks in run {run['id']} at {head}") + return 0 + + +def cmd_promote(args: argparse.Namespace) -> int: + bundle = json.loads(BUNDLE_PATH.read_text()) + problems: list[str] = [] + receipts = latest_receipts(Path(args.ledger), problems=problems) + for problem in problems: + # Such a pair is left out of the plan until its files are rederived. + print(f"::warning::{problem}") + branches = _open_candidate_branches() if args.apply else {} + plan = plan_promotion(receipts, bundle, branches) + print(f"promotion: {plan.reason}") + if plan.target is not None: + print( + f"target {plan.ready_branch or 'not promotable now'} " + f"(pair {plan.target.pair_id[:12]}, commit {plan.ready_commit})" + ) + for branch in plan.close_branches: + print(f"superseded {branch}") + if not args.apply: + return 0 + for branch in plan.close_branches: + subprocess.run( + [ + "gh", + "pr", + "close", + branch, + "--repo", + LEDGER_REPOSITORY, + "--comment", + f"Superseded: {plan.reason}. The ledger keeps this candidate's evidence.", + ], + check=False, + ) + if plan.ready_branch is not None and plan.ready_commit is not None: + subprocess.run( + ["gh", "pr", "ready", plan.ready_branch, "--repo", LEDGER_REPOSITORY], + check=False, + ) + if config()["promotion"]["automatic"]: + # GitHub refuses the merge if the branch moved off the qualified + # commit between the plan and the merge. + subprocess.run( + [ + "gh", + "pr", + "merge", + plan.ready_branch, + "--repo", + LEDGER_REPOSITORY, + "--auto", + "--squash", + "--match-head-commit", + plan.ready_commit, + ], + check=False, + ) + return 0 + + +def cmd_accept(args: argparse.Namespace) -> int: + pair_dir = pair_directory(Path(args.ledger), "uk", args.pair_id) + path = pair_dir / "qualification_receipt.json" + # Accept only a receipt that is an attempt's own, never a merged one. + receipt = published_receipt(pair_dir) + reviewed = accept_review( + receipt, + reviewer=args.reviewer or os.environ.get("GITHUB_ACTOR", ""), + accepted_receipt_sha256=args.receipt_sha256, + explanation=args.explanation, + accepted_at=utc_now(), + ) + write_json(path, reviewed.canonical_payload()) + write_json( + path.parent / "reviews" / f"{reviewed.receipt_sha256}.json", + reviewed.canonical_payload(), + ) + print( + f"accepted {args.pair_id[:12]} by {reviewed.review.reviewer}; promotable={reviewed.promotable}" + ) + return 0 + + +def parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + commands = parser.add_subparsers(dest="command", required=True) + + resolve = commands.add_parser("resolve") + resolve.add_argument("--model-version") + resolve.add_argument("--data-version") + resolve.add_argument("--ledger") + resolve.add_argument("--force", action="store_true") + resolve.add_argument("--out", required=True) + resolve.set_defaults(func=cmd_resolve) + + prepare = commands.add_parser("prepare") + prepare.add_argument("--candidate", required=True) + prepare.add_argument("--out", default="prepared.json") + prepare.add_argument("--skip-snapshots", action="store_true") + prepare.set_defaults(func=cmd_prepare) + + stage = commands.add_parser("stage") + stage.add_argument("--candidate", required=True) + stage.add_argument("--ledger", required=True) + stage.set_defaults(func=cmd_stage) + + gates = commands.add_parser("gates") + gates.add_argument("--attempt", required=True) + gates.add_argument("--work-dir", default="qualification-work") + gates.set_defaults(func=cmd_gates) + + request = commands.add_parser("request-replay") + request.add_argument("--attempt", required=True) + request.add_argument("--replay-attempt", type=int, default=1) + request.add_argument("--dispatch", action="store_true") + request.set_defaults(func=cmd_request_replay) + + poll = commands.add_parser("poll-replay") + poll.add_argument("--attempt", required=True) + poll.add_argument("--wait-minutes", type=float, default=0) + poll.add_argument("--interval-seconds", type=float, default=300) + poll.set_defaults(func=cmd_poll_replay) + + finalize = commands.add_parser("finalize") + finalize.add_argument("--attempt", required=True) + finalize.add_argument("--ledger", required=True) + finalize.add_argument("--pr-body") + finalize.set_defaults(func=cmd_finalize) + + waiting = commands.add_parser("waiting") + waiting.add_argument("--ledger", required=True) + waiting.set_defaults(func=cmd_waiting) + + refresh = commands.add_parser("refresh-pr-checks") + refresh.add_argument("--ledger", required=True) + refresh.set_defaults(func=cmd_refresh_pr_checks) + + recoverable = commands.add_parser("recoverable") + recoverable.add_argument("--ledger", required=True) + recoverable.set_defaults(func=cmd_recoverable) + + promote = commands.add_parser("promote") + promote.add_argument("--ledger", required=True) + promote.add_argument("--apply", action="store_true") + promote.set_defaults(func=cmd_promote) + + accept = commands.add_parser("accept") + accept.add_argument("--ledger", required=True) + accept.add_argument("--pair-id", required=True) + accept.add_argument("--receipt-sha256", required=True) + accept.add_argument("--explanation", required=True) + accept.add_argument("--reviewer") + accept.set_defaults(func=cmd_accept) + + rederive = commands.add_parser("rederive-ledger") + rederive.add_argument("ledger") + rederive.set_defaults(func=cmd_rederive_ledger) + return parser + + +def cmd_rederive_ledger(args: argparse.Namespace) -> int: + """Recompute every pair's published files from its attempts. + + Called by .github/qualification-ledger.sh after a concurrent publish + changed a pair's published files on both sides. + """ + + for pair_dir in sorted((Path(args.ledger) / "uk").glob("*")): + if pair_dir.is_dir(): + rederive_publication(pair_dir) + return 0 + + +def main(argv: Optional[list[str]] = None) -> int: + args = parser().parse_args(argv) + return int(args.func(args) or 0) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/generate_trace_tros.py b/scripts/generate_trace_tros.py index a9541b46..b34562f8 100644 --- a/scripts/generate_trace_tros.py +++ b/scripts/generate_trace_tros.py @@ -76,7 +76,17 @@ def _validate_reviewed_inputs( ): raise ValueError(f"{prefix} requires certification for the pinned model") validate_release_manifest( - data, country.model_package.name, country.model_package.version + data, + country.model_package.name, + country.model_package.version, + # Revalidate the certificate's exact qualification context, including + # qualification-only models/cores. Never substitute a publisher range. + core_version=country.certification.certified_for_core_version, + qualified_pair_id=( + country.certification.qualification_pair_id + if country.certification.compatibility_basis == "qualified_exact_pair" + else None + ), ) certified = country.certified_data_artifact artifact = data.artifacts.get(certified.dataset) if certified else None @@ -127,6 +137,17 @@ def _validate_runtime_model(bundle: dict, country: CountryReleaseManifest) -> No component = bundle.get("packages", {}).get(key, {}) model = country.model_package prefix = f"{country.country_id}: strict TRACE release" + certification = country.certification + if certification and certification.compatibility_basis == "qualified_exact_pair": + core = bundle.get("packages", {}).get("policyengine-core", {}) + if ( + not certification.qualification_pair_id + or not certification.certified_for_core_version + or core.get("version") != certification.certified_for_core_version + ): + raise ValueError( + f"{prefix}: runtime core differs from the qualified exact pair" + ) if ( model.name != key or component.get("name") != model.name diff --git a/scripts/release_consumers.py b/scripts/release_consumers.py new file mode 100644 index 00000000..57ab70cf --- /dev/null +++ b/scripts/release_consumers.py @@ -0,0 +1,164 @@ +"""Build the post-release dispatches for policyengine.py consumers. + +Run by the NotifyConsumers job in ``.github/workflows/push.yaml`` after a +release is visible on PyPI:: + + python scripts/release_consumers.py payload --version 6.2.7 > payload.json + python scripts/release_consumers.py scorecard-request --version 6.2.7 > request.json + +``payload`` is the ``policyengine-release`` repository dispatch (version, +certified UK bundle identity, qualification digests). ``scorecard-request`` +is the scorecard's ``released_bundle`` replay request; it needs the release +installed (``pip install "policyengine[uk]=="``) and +``HUGGING_FACE_TOKEN``. +""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(REPO_ROOT / "src")) +os.environ.setdefault("POLICYENGINE_SKIP_COUNTRY_IMPORTS", "1") + +import requests # noqa: E402 + +from policyengine.provenance.qualification_ledger import ( # noqa: E402 + fetch_ledger_receipt, +) +from policyengine.provenance.uk_qualification import ( # noqa: E402 + release_dispatch_payload, + released_bundle_request, +) + +BUNDLE_PATH = REPO_ROOT / "src" / "policyengine" / "data" / "bundle" / "manifest.json" +TIMEOUT = 60 +TOKEN_VARIABLES = ("HF_TOKEN", "HUGGING_FACE_TOKEN", "HUGGINGFACE_HUB_TOKEN") + + +def _bundle() -> dict: + return json.loads(BUNDLE_PATH.read_text()) + + +def _pypi(path: str) -> dict: + response = requests.get(f"https://pypi.org/pypi/{path}/json", timeout=TIMEOUT) + response.raise_for_status() + return response.json() + + +def _hf_file(repo_id: str, revision: str, path: str) -> bytes: + token = os.environ.get("HUGGING_FACE_TOKEN") or os.environ.get("HF_TOKEN") + response = requests.get( + f"https://huggingface.co/{repo_id}/resolve/{revision}/{path}", + headers={"Authorization": f"Bearer {token}"} if token else {}, + timeout=TIMEOUT, + ) + response.raise_for_status() + return response.content + + +def cmd_payload(args: argparse.Namespace) -> int: + payload = release_dispatch_payload( + _bundle(), version=args.version, fetch_receipt=fetch_ledger_receipt + ) + print(json.dumps(payload, sort_keys=True)) + return 0 + + +def _installed(code: str, *, strip_tokens: bool) -> str: + """Run ``code`` against the installed release, outside the checkout.""" + + env = dict(os.environ) + env.pop("POLICYENGINE_SKIP_COUNTRY_IMPORTS", None) + if strip_tokens: + for key in TOKEN_VARIABLES: + env.pop(key, None) + return subprocess.run( + [sys.executable, "-c", code], + env=env, + cwd="/", + check=True, + capture_output=True, + text=True, + ).stdout.strip() + + +def cmd_scorecard_request(args: argparse.Namespace) -> int: + release = _pypi(f"policyengine/{args.version}") + wheels = [item for item in release["urls"] if item["packagetype"] == "bdist_wheel"] + if len(wheels) != 1: + raise SystemExit(f"policyengine {args.version} must publish exactly one wheel.") + installed_version = _installed( + "from importlib.metadata import version; print(version('policyengine'))", + strip_tokens=False, + ) + if installed_version != args.version: + raise SystemExit( + f"policyengine {installed_version} is installed, not {args.version}." + ) + manifest_path = _installed( + "from importlib.resources import files; " + "print(files('policyengine').joinpath('data', 'bundle', 'manifest.json'))", + strip_tokens=False, + ) + release_bundle = json.loads( + _installed( + "import json, policyengine as pe; " + "print(json.dumps(dict(pe.uk.uk_latest.release_bundle), sort_keys=True))", + strip_tokens=True, + ).splitlines()[-1] + ) + server = os.environ.get("GITHUB_SERVER_URL", "https://github.com") + repository = os.environ.get("GITHUB_REPOSITORY", "PolicyEngine/policyengine.py") + run_id = os.environ.get("GITHUB_RUN_ID", "1") + request = released_bundle_request( + json.loads(Path(manifest_path).read_text()), + version=args.version, + wheel_sha256=wheels[0]["digests"]["sha256"], + packaged_manifest_sha256=hashlib.sha256( + Path(manifest_path).read_bytes() + ).hexdigest(), + release_bundle=release_bundle, + hf_file=_hf_file, + pypi_project=_pypi, + requested_by={ + "repository": repository, + "workflow_run_id": int(run_id), + "run_attempt": int(os.environ.get("GITHUB_RUN_ATTEMPT", "1")), + "commit": os.environ.get("GITHUB_SHA", "0" * 40), + "run_url": f"{server}/{repository}/actions/runs/{run_id}", + }, + ) + print( + json.dumps( + { + "event_type": "uk-replay-qualification-request", + "client_payload": {"request": request.document()}, + }, + sort_keys=True, + ) + ) + return 0 + + +def main(argv=None) -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + commands = parser.add_subparsers(dest="command", required=True) + payload = commands.add_parser("payload") + payload.add_argument("--version", required=True) + payload.set_defaults(func=cmd_payload) + scorecard = commands.add_parser("scorecard-request") + scorecard.add_argument("--version", required=True) + scorecard.set_defaults(func=cmd_scorecard_request) + args = parser.parse_args(argv) + return int(args.func(args) or 0) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/src/policyengine/data/qualification/uk.json b/src/policyengine/data/qualification/uk.json new file mode 100644 index 00000000..d50e0eba --- /dev/null +++ b/src/policyengine/data/qualification/uk.json @@ -0,0 +1,40 @@ +{ + "schema_version": 1, + "country_id": "uk", + "required_years": [2025, 2026, 2027, 2028, 2029, 2030], + "budget_horizon_years": [2026, 2027, 2028, 2029, 2030, 2031], + "reform_contract": { + "entrypoint": "policyengine.tax_benefit_models.uk.budget_reform_contract:run_budget_reform_contract", + "seed": 0, + "max_examples": 20, + "_comment": "W1's runner (policyengine.py#577); its pass also needs policyengine-uk#2305 and policyengine.py#575, so certified 2.125.1 fails the dated cases by design." + }, + "metrics": { + "tool": { + "repository": "PolicyEngine/policyengine-uk", + "commit": "1191ac5a473419f48e0c0643f83d31719da788e1", + "path": "policyengine_uk/utils/aggregate_impact", + "_comment": "W3's aggregate-impact tool, pinned so both pairs use the same definitions: the approved head of draft policyengine-uk#2335. It suppresses figures from fewer than 10 records and gives each total's excluded figures with their signs (exclude_signs), which the metrics review requires. #2335 merges after cos d1601; move the pin to the merged commit when it lands, and never to a commit without suppression." + }, + "years": [2025, 2026, 2027, 2028, 2029, 2030, 2031], + "bounds": "uk_review_bounds.json" + }, + "budget_inputs": { + "critical": "uk_budget_critical_inputs.json" + }, + "replay": { + "repository": "PolicyEngine/policyengine-scorecard", + "event_type": "uk-replay-qualification-request", + "state_ref": "replay-state", + "compare_to": "latest_certified", + "poll_timeout_minutes": 360 + }, + "reform_responsiveness": { + "_comment": "Plan invariant 'Reform responsiveness' (11 Oct 2026): a reform the model silently ignores must fail qualification. The standard set, each reform's target variable, direction and yearly minimum are the metrics tool's own (standard_reforms.yaml in the pinned tool; at 228a05a3e each minimum is a fifth of the reform's smallest yearly effect on the certified pair, adopted under cos d1344). The tool costs the set by default and emits reform_runs: and responsive: checks. This gate requires both checks to be present and passing for every standard reform in the candidate's report, and recomputes each responsive verdict from the report's reform_targets. values.", + "standard_reforms_file": "standard_reforms.yaml" + }, + "promotion": { + "automatic": false, + "decision": "cos d1344: automatic promotion and review bounds are methodology calls; until ruled, promotion is a reviewed PR merge." + } +} diff --git a/src/policyengine/data/qualification/uk_budget_critical_inputs.json b/src/policyengine/data/qualification/uk_budget_critical_inputs.json new file mode 100644 index 00000000..90d2e422 --- /dev/null +++ b/src/policyengine/data/qualification/uk_budget_critical_inputs.json @@ -0,0 +1,58 @@ +{ + "_comment": "Proposed Budget-critical input variables, pending review under cos d1344. Each name was checked on 11 October 2026 to be an input variable (Variable.is_input_variable()) in policyengine-uk 2.125.1 and a non-degenerate column of uk-data 1.58.0 enhanced_frs_2024_25.h5. A candidate fails the budget_inputs gate if any is missing or degenerate when it was usable in the previous certified pair; a gap present in both pairs needs review unless listed under accepted_gaps with an issue.", + "critical": [ + "accommodation_type", + "age", + "alcohol_and_tobacco_consumption", + "carers_allowance_reported", + "child_benefit_reported", + "child_tax_credit_reported", + "childcare_expenses", + "clothing_and_footwear_consumption", + "communication_consumption", + "corporate_wealth", + "council_tax", + "council_tax_band", + "diesel_spending", + "dividend_income", + "domestic_energy_consumption", + "education_consumption", + "employer_pension_contributions", + "food_and_non_alcoholic_beverages_consumption", + "gender", + "gross_financial_wealth", + "health_consumption", + "hours_worked", + "household_furnishings_consumption", + "household_owns_tv", + "household_weight", + "housing_benefit_reported", + "housing_water_and_electricity_consumption", + "is_disabled_for_benefits", + "main_residence_value", + "miscellaneous_consumption", + "net_financial_wealth", + "num_vehicles", + "other_residential_property_value", + "pension_credit_reported", + "personal_pension_contributions", + "petrol_spending", + "private_pension_income", + "property_income", + "property_purchased", + "recreation_consumption", + "region", + "rent", + "restaurants_and_hotels_consumption", + "savings", + "savings_interest_income", + "self_employment_income", + "tenure_type", + "transport_consumption", + "universal_credit_reported", + "working_tax_credit_reported", + "would_claim_child_benefit", + "would_claim_uc" + ], + "accepted_gaps": {} +} diff --git a/src/policyengine/data/qualification/uk_review_bounds.json b/src/policyengine/data/qualification/uk_review_bounds.json new file mode 100644 index 00000000..c1c7718b --- /dev/null +++ b/src/policyengine/data/qualification/uk_review_bounds.json @@ -0,0 +1,79 @@ +{ + "_comment": "Proposed per-kind review bounds for qualification metrics, pending cos d1344. A move needs review when |candidate - previous| exceeds max(relative * |previous|, absolute). Units follow the metric report: gbp (pounds), count (people or units), rate and share (fractions, so 0.005 is half a percentage point). They are review triggers, not acceptance criteria: a move inside the bounds is still in the evidence.", + "default": { + "relative": 0.02, + "absolute": 500000000.0 + }, + "kinds": { + "receipt": { + "relative": 0.02, + "absolute": 500000000.0, + "why": "2% or \u00a30.5bn: the Budget scorecard reports measures to \u00a30.1bn, and model fixes moving a head by more than 2% should be explained." + }, + "spending": { + "relative": 0.02, + "absolute": 500000000.0, + "why": "As receipts." + }, + "balance": { + "relative": 0.02, + "absolute": 500000000.0, + "why": "As receipts." + }, + "model_total": { + "relative": 0.02, + "absolute": 500000000.0, + "why": "As receipts." + }, + "household_total": { + "relative": 0.02, + "absolute": 500000000.0, + "why": "As receipts." + }, + "caseload": { + "relative": 0.03, + "absolute": 50000.0, + "why": "3% or 50,000 claimants: caseloads are calibrated targets; larger moves signal data or eligibility changes." + }, + "population": { + "relative": 0.01, + "absolute": 100000.0, + "why": "Population totals should barely move between pairs." + }, + "poverty": { + "relative": 0.0, + "absolute": 0.005, + "why": "Half a percentage point on a rate." + }, + "poverty_count": { + "relative": 0.03, + "absolute": 100000.0, + "why": "3% or 100,000 people." + }, + "inequality": { + "relative": 0.0, + "absolute": 0.005, + "why": "0.005 on a Gini-type index." + }, + "decile": { + "relative": 0.0, + "absolute": 0.005, + "why": "Half a point on a decile share." + }, + "double_count": { + "relative": 0.0, + "absolute": 1.0, + "why": "Any change in a counted-once check needs review." + }, + "reform": { + "relative": 0.05, + "absolute": 100000000.0, + "why": "5% or \u00a30.1bn on a reference reform costing." + }, + "reform_target": { + "relative": 0.05, + "absolute": 100000000.0, + "why": "5% or \u00a30.1bn on a standard reform's change in its target variable." + } + } +} diff --git a/src/policyengine/data/schemas/vendor/policyengine-scorecard/README.md b/src/policyengine/data/schemas/vendor/policyengine-scorecard/README.md new file mode 100644 index 00000000..b8a52769 --- /dev/null +++ b/src/policyengine/data/schemas/vendor/policyengine-scorecard/README.md @@ -0,0 +1,9 @@ +# Vendored policyengine-scorecard schemas + +Copied unchanged from PolicyEngine/policyengine-scorecard, branch +`w6/replay-variants`, commit `dc040de` (`docs/uk_replay/schemas/`): the v1 +pre-promotion replay interface between policyengine.py qualification (W4) +and the replay corpus (W6). `policyengine.provenance.qualification` mirrors +them, and `tests/test_qualification_contracts.py` checks the mirrors and the +schemas accept and reject the same documents. Refresh by copying from a newer +scorecard commit and updating the commit above; never edit them by hand. diff --git a/src/policyengine/data/schemas/vendor/policyengine-scorecard/qualification_request.v1.json b/src/policyengine/data/schemas/vendor/policyengine-scorecard/qualification_request.v1.json new file mode 100644 index 00000000..5865ca3e --- /dev/null +++ b/src/policyengine/data/schemas/vendor/policyengine-scorecard/qualification_request.v1.json @@ -0,0 +1,145 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/PolicyEngine/policyengine-scorecard/blob/main/docs/uk_replay/schemas/qualification_request.v1.json", + "title": "UK fiscal-event replay qualification request, v1", + "description": "Sent by policyengine.py release qualification (W4) to policyengine-scorecard (W6). request_id is the SHA-256 of the RFC 8785 (JCS) bytes of the whole request; it is never a field. Additive optional fields keep v1; renamed or removed fields bump the version.", + "type": "object", + "additionalProperties": false, + "required": ["schema", "kind", "attempt", "candidate", "compare_to", "requested_by"], + "properties": { + "schema": {"const": "policyengine.uk-replay-qualification-request/v1"}, + "kind": { + "description": "candidate: a pair not yet promoted; released_bundle: a published policyengine.py release.", + "enum": ["candidate", "released_bundle"] + }, + "attempt": { + "description": "A retry of a failed request is a new request with a higher attempt.", + "type": "integer", + "minimum": 1 + }, + "candidate": { + "type": "object", + "additionalProperties": false, + "required": ["policyengine", "policyengine_uk", "policyengine_core", "data", "expected_release_bundle"], + "properties": { + "policyengine": { + "type": "object", + "additionalProperties": false, + "required": ["version", "source", "packaged_bundle_manifest_sha256"], + "properties": { + "version": {"$ref": "#/$defs/version"}, + "source": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["git"], + "properties": { + "git": { + "type": "object", + "additionalProperties": false, + "required": ["repository", "commit"], + "properties": { + "repository": {"const": "PolicyEngine/policyengine.py"}, + "commit": {"$ref": "#/$defs/sha1"} + } + } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["pypi"], + "properties": { + "pypi": { + "type": "object", + "additionalProperties": false, + "required": ["wheel_sha256"], + "properties": {"wheel_sha256": {"$ref": "#/$defs/sha256"}} + } + } + } + ] + }, + "packaged_bundle_manifest_sha256": {"$ref": "#/$defs/sha256"} + } + }, + "policyengine_uk": {"$ref": "#/$defs/pypi_package"}, + "policyengine_core": {"$ref": "#/$defs/pypi_package"}, + "data": { + "type": "object", + "additionalProperties": false, + "required": ["build_id", "dataset", "repo_id", "repo_type", "revision", "path", "sha256", "size_bytes", "release_manifest_sha256"], + "properties": { + "build_id": {"type": "string", "minLength": 1}, + "dataset": {"type": "string", "minLength": 1}, + "repo_id": {"type": "string", "pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$"}, + "repo_type": {"enum": ["model", "dataset"]}, + "revision": {"type": "string", "minLength": 1}, + "path": {"type": "string", "minLength": 1, "pattern": "^(?!.*(^|/)\\.\\.(/|$))[^/][^\\\\]*$"}, + "sha256": {"$ref": "#/$defs/sha256"}, + "size_bytes": {"type": "integer", "minimum": 1}, + "release_manifest_sha256": {"$ref": "#/$defs/sha256"} + } + }, + "expected_release_bundle": { + "description": "The exact dict(pe.uk.uk_latest.release_bundle) under packaged certification: computed in a subprocess with HF_TOKEN, HUGGING_FACE_TOKEN, HUGGINGFACE_HUB_TOKEN, NO_PROXY and no_proxy removed and HTTP_PROXY, HTTPS_PROXY, ALL_PROXY (and their lowercase forms) set to http://127.0.0.1:9, so the wrapper's release-manifest request cannot reach the network and the packaged certification is used for a private or public data repository (pipeline/uk_replay_variants.py, isolate_certification_environment).", + "type": "object", + "required": ["policyengine_version", "model_version", "certified_data_build_id", "certified_data_artifact_sha256", "default_dataset_uri"], + "additionalProperties": {"type": ["string", "null"]}, + "properties": { + "policyengine_version": {"$ref": "#/$defs/version"}, + "model_version": {"$ref": "#/$defs/version"}, + "certified_data_build_id": {"type": "string", "minLength": 1}, + "certified_data_artifact_sha256": {"$ref": "#/$defs/sha256"}, + "default_dataset_uri": {"type": "string", "pattern": "^hf://"} + } + } + } + }, + "compare_to": { + "description": "latest_certified, or an explicit scorecard variant_id.", + "type": "string", + "minLength": 1 + }, + "requested_by": { + "type": "object", + "additionalProperties": true, + "required": ["repository", "commit"], + "properties": { + "repository": {"type": "string", "pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$"}, + "workflow_run_id": {"type": "integer", "minimum": 1}, + "run_attempt": {"type": "integer", "minimum": 1}, + "commit": {"$ref": "#/$defs/sha1"} + } + } + }, + "allOf": [ + { + "if": {"properties": {"kind": {"const": "released_bundle"}}}, + "then": { + "properties": { + "candidate": { + "properties": { + "policyengine": {"properties": {"source": {"required": ["pypi"]}}} + } + } + } + } + } + ], + "$defs": { + "sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "sha1": {"type": "string", "pattern": "^[0-9a-f]{40}$"}, + "version": {"type": "string", "pattern": "^[0-9]+(\\.[0-9]+)*([a-z0-9.+-]*)$"}, + "pypi_package": { + "type": "object", + "additionalProperties": false, + "required": ["version", "wheel_sha256"], + "properties": { + "version": {"$ref": "#/$defs/version"}, + "wheel_sha256": {"$ref": "#/$defs/sha256"} + } + } + } +} diff --git a/src/policyengine/data/schemas/vendor/policyengine-scorecard/replay_receipt.v1.json b/src/policyengine/data/schemas/vendor/policyengine-scorecard/replay_receipt.v1.json new file mode 100644 index 00000000..a9d52614 --- /dev/null +++ b/src/policyengine/data/schemas/vendor/policyengine-scorecard/replay_receipt.v1.json @@ -0,0 +1,638 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/PolicyEngine/policyengine-scorecard/blob/main/docs/uk_replay/schemas/replay_receipt.v1.json", + "title": "UK fiscal-event replay receipt, v1", + "description": "Returned by policyengine-scorecard for one qualification request. receipt_sha256 is the SHA-256 of the RFC 8785 (JCS) bytes of the receipt without that field. Flags describe movements against the base variant; they carry no verdict.", + "type": "object", + "additionalProperties": false, + "required": [ + "schema", + "request_id", + "status", + "rejection", + "variant_id", + "variant_identity_sha256", + "execution", + "corpus", + "integrity", + "diff", + "results", + "receipt_sha256" + ], + "properties": { + "schema": { + "const": "policyengine.uk-replay-receipt/v1" + }, + "request_id": { + "$ref": "#/$defs/sha256" + }, + "status": { + "enum": [ + "complete", + "rejected", + "failed" + ] + }, + "rejection": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "code", + "detail" + ], + "properties": { + "code": { + "enum": [ + "schema_invalid", + "artifact_hash_mismatch", + "release_bundle_mismatch", + "incompatible_pair", + "data_access_failed", + "no_supported_years" + ] + }, + "detail": { + "type": "string", + "minLength": 1 + } + } + } + ] + }, + "variant_id": { + "type": [ + "string", + "null" + ], + "minLength": 1 + }, + "variant_identity_sha256": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "#/$defs/sha256" + } + ] + }, + "execution": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "packages", + "packaged_bundle_manifest_sha256", + "release_bundle", + "dataset_sha256", + "requirements_sha256", + "python", + "platform", + "runner" + ], + "properties": { + "packages": { + "type": "object", + "additionalProperties": false, + "required": [ + "policyengine", + "policyengine-uk", + "policyengine-core" + ], + "properties": { + "policyengine": { + "$ref": "#/$defs/installed_package" + }, + "policyengine-uk": { + "$ref": "#/$defs/installed_package" + }, + "policyengine-core": { + "$ref": "#/$defs/installed_package" + } + } + }, + "packaged_bundle_manifest_sha256": { + "$ref": "#/$defs/sha256" + }, + "release_bundle": { + "type": "object", + "additionalProperties": { + "type": [ + "string", + "null" + ] + } + }, + "dataset_sha256": { + "$ref": "#/$defs/sha256" + }, + "requirements_sha256": { + "$ref": "#/$defs/sha256" + }, + "python": { + "type": "string", + "pattern": "^3\\.[0-9]+\\.[0-9]+$" + }, + "platform": { + "type": "string", + "minLength": 1 + }, + "runner": { + "type": "string", + "minLength": 1 + } + } + } + ] + }, + "corpus": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "corpus_version", + "events" + ], + "properties": { + "corpus_version": { + "type": "string", + "minLength": 1 + }, + "events": { + "type": "object", + "minProperties": 1, + "additionalProperties": { + "$ref": "#/$defs/event_result" + } + } + } + } + ] + }, + "integrity": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "failures" + ], + "properties": { + "status": { + "description": "complete: every supported measure-year ran and every receipt verified. incomplete: some supported pairs are missing. failed: a crash, a non-finite value, an identity mismatch after the run started, or a broken accounting identity.", + "enum": [ + "complete", + "incomplete", + "failed", + "not_run" + ] + }, + "failures": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "code", + "detail" + ], + "properties": { + "code": { + "type": "string", + "minLength": 1 + }, + "detail": { + "type": "string", + "minLength": 1 + }, + "event": { + "type": "string" + }, + "measure_key": { + "type": "string" + }, + "year": { + "type": "integer" + } + } + } + } + } + }, + "diff": { + "oneOf": [ + { + "type": "null" + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "base_variant_id", + "diff_sha256", + "flag_counts", + "flagged_rows" + ], + "properties": { + "base_variant_id": { + "type": "string", + "minLength": 1 + }, + "diff_sha256": { + "$ref": "#/$defs/sha256" + }, + "flag_counts": { + "type": "object", + "additionalProperties": false, + "required": [ + "numerical", + "structural", + "coverage", + "mapping", + "support", + "inertness", + "construction" + ], + "properties": { + "numerical": { + "type": "integer", + "minimum": 0 + }, + "structural": { + "type": "integer", + "minimum": 0 + }, + "coverage": { + "type": "integer", + "minimum": 0 + }, + "mapping": { + "type": "integer", + "minimum": 0 + }, + "support": { + "type": "integer", + "minimum": 0 + }, + "inertness": { + "type": "integer", + "minimum": 0 + }, + "construction": { + "type": "integer", + "minimum": 0 + } + } + }, + "flagged_rows": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "event", + "measure_key", + "year", + "level", + "key", + "flags", + "base_gbp", + "head_gbp" + ], + "properties": { + "event": { + "type": "string", + "minLength": 1 + }, + "measure_key": { + "type": "string", + "minLength": 1 + }, + "year": { + "type": "integer" + }, + "level": { + "enum": [ + "source_row", + "measure_year", + "head" + ] + }, + "key": { + "type": "string", + "minLength": 1 + }, + "flags": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "enum": [ + "numerical", + "structural", + "coverage", + "mapping", + "support", + "inertness", + "construction" + ] + } + }, + "base_gbp": { + "type": [ + "number", + "null" + ] + }, + "head_gbp": { + "type": [ + "number", + "null" + ] + } + } + } + } + } + } + ] + }, + "results": { + "type": "object", + "additionalProperties": false, + "required": [ + "repository", + "ref", + "path" + ], + "properties": { + "repository": { + "const": "PolicyEngine/policyengine-scorecard" + }, + "ref": { + "type": "string", + "minLength": 1 + }, + "commit": { + "$ref": "#/$defs/sha1" + }, + "path": { + "type": "string", + "minLength": 1 + }, + "manifest_sha256": { + "description": "SHA-256 of NAMESPACE_MANIFEST.json, which lists the SHA-256 of every file in the sealed result namespace except itself and the receipt.", + "$ref": "#/$defs/sha256" + } + } + }, + "receipt_sha256": { + "$ref": "#/$defs/sha256" + } + }, + "allOf": [ + { + "if": { + "properties": { + "status": { + "const": "rejected" + } + } + }, + "then": { + "properties": { + "rejection": { + "type": "object" + }, + "corpus": { + "type": "null" + }, + "diff": { + "type": "null" + }, + "variant_id": { + "type": "null" + }, + "variant_identity_sha256": { + "type": "null" + }, + "execution": { + "type": "null" + }, + "integrity": { + "properties": { + "status": { + "const": "not_run" + } + } + } + } + } + }, + { + "if": { + "properties": { + "status": { + "const": "complete" + } + } + }, + "then": { + "properties": { + "rejection": { + "type": "null" + }, + "variant_id": { + "type": "string" + }, + "execution": { + "type": "object" + }, + "variant_identity_sha256": { + "$ref": "#/$defs/sha256" + }, + "results": { + "required": [ + "manifest_sha256" + ] + } + } + } + }, + { + "if": { + "properties": { + "status": { + "const": "complete" + } + } + }, + "then": { + "properties": { + "integrity": { + "properties": { + "status": { + "enum": [ + "complete", + "incomplete" + ] + } + } + }, + "corpus": { + "type": "object" + } + } + } + }, + { + "if": { + "properties": { + "status": { + "const": "failed" + } + } + }, + "then": { + "properties": { + "rejection": { + "type": "null" + }, + "integrity": { + "properties": { + "status": { + "const": "failed" + } + } + } + } + } + } + ], + "$defs": { + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$" + }, + "sha1": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "installed_package": { + "type": "object", + "additionalProperties": false, + "required": [ + "version", + "record_sha256" + ], + "properties": { + "version": { + "type": "string", + "minLength": 1 + }, + "record_sha256": { + "$ref": "#/$defs/sha256" + }, + "wheel_sha256": { + "$ref": "#/$defs/sha256" + } + } + }, + "event_result": { + "type": "object", + "additionalProperties": false, + "required": [ + "registry_sha256", + "supported_years", + "unsupported_years", + "run_manifest_sha256", + "staging_manifest_sha256", + "comparison_provenance_sha256", + "artifacts", + "full_supported_grid_complete", + "inert_measure_years" + ], + "properties": { + "registry_sha256": { + "$ref": "#/$defs/sha256" + }, + "supported_years": { + "type": "array", + "items": { + "type": "integer" + }, + "uniqueItems": true + }, + "unsupported_years": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "year", + "reason" + ], + "properties": { + "year": { + "type": "integer" + }, + "reason": { + "enum": [ + "before_data_year", + "after_extension_end" + ] + } + } + } + }, + "run_manifest_sha256": { + "$ref": "#/$defs/sha256" + }, + "staging_manifest_sha256": { + "$ref": "#/$defs/sha256" + }, + "comparison_provenance_sha256": { + "$ref": "#/$defs/sha256" + }, + "artifacts": { + "type": "integer", + "minimum": 0 + }, + "full_supported_grid_complete": { + "type": "boolean" + }, + "inert_measure_years": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "measure_key", + "year" + ], + "properties": { + "measure_key": { + "type": "string" + }, + "year": { + "type": "integer" + } + } + } + } + } + } + } +} diff --git a/src/policyengine/data/schemas/vendor/policyengine-scorecard/replay_status.v1.json b/src/policyengine/data/schemas/vendor/policyengine-scorecard/replay_status.v1.json new file mode 100644 index 00000000..3694ca4e --- /dev/null +++ b/src/policyengine/data/schemas/vendor/policyengine-scorecard/replay_status.v1.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://github.com/PolicyEngine/policyengine-scorecard/blob/main/docs/uk_replay/schemas/replay_status.v1.json", + "title": "UK fiscal-event replay request status, v1", + "description": "replay-state:requests//STATUS.json. A terminal status names the receipt's digest and the replay-state commit that added REPLAY_RECEIPT.json, so one contents read binds the status to the receipt bytes.", + "type": "object", + "additionalProperties": false, + "required": ["schema", "request_id", "status", "history"], + "properties": { + "schema": {"const": "policyengine.uk-replay-status/v1"}, + "request_id": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "status": {"enum": ["queued", "running", "complete", "rejected", "failed"]}, + "history": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["status", "at", "run_url"], + "properties": { + "status": {"enum": ["queued", "running", "complete", "rejected", "failed"]}, + "at": {"type": "string", "format": "date-time"}, + "run_url": {"type": "string", "pattern": "^https://github\\.com/PolicyEngine/policyengine-scorecard/actions/runs/[0-9]+"} + } + } + }, + "receipt_sha256": {"type": "string", "pattern": "^[0-9a-f]{64}$"}, + "receipt_commit": {"type": "string", "pattern": "^[0-9a-f]{40}$"} + }, + "allOf": [ + { + "if": {"properties": {"status": {"enum": ["complete", "rejected", "failed"]}}}, + "then": {"required": ["receipt_sha256", "receipt_commit"]} + } + ] +} diff --git a/src/policyengine/provenance/certification.py b/src/policyengine/provenance/certification.py index 3b4b27f1..e31762f9 100644 --- a/src/policyengine/provenance/certification.py +++ b/src/policyengine/provenance/certification.py @@ -69,6 +69,14 @@ # fallback there exempts exactly this string from the fingerprint identity # check, so a claims-certified bundle stays importable without network access. BASIS_PUBLISHER_CLAIM = "legacy_compatible_model_package" +# The pair passed policyengine.py's automated candidate qualification. The +# claim is exact (``==`` model and core versions, one data release) and is +# valid only with a promotable qualification receipt for ``qualification_pair_id`` +# in the ledger; ``tests/test_certification_evidence.py`` refuses it otherwise. +BASIS_QUALIFIED_PAIR = "qualified_exact_pair" +QUALIFICATION_LEDGER_URL = ( + "https://github.com/PolicyEngine/policyengine.py/tree/uk-qualification-ledger" +) POPULACE_US_SOURCE_COVERAGE_FILE = "us_source_coverage.json" # The Populace US producer publishes under `microcosm-data` since the # source-enrichment releases; `populace-data` is its earlier name. Both denote @@ -226,15 +234,26 @@ def validate_release_manifest( manifest: DataReleaseManifest, model_package: str, model_version: str, + *, + core_version: Optional[str] = None, + qualified_pair_id: Optional[str] = None, ) -> tuple[str, list[str]]: """Hard checks raise; returns ``(compatibility_basis, warnings)``. The certification gate (docs/release-bundles.md): a data release may be certified for a model version only when the model exactly matches - the build-time model, or the publisher's ``compatible_model_packages`` - covers it. The publisher-claim basis is recorded and warned about — - it is the data publisher's assertion, made good only by this repo's - test suite passing on the pinned pair. + the build-time model, the publisher's ``compatible_model_packages`` + covers it, or (``qualified_pair_id``) policyengine.py's automated + qualification tested exactly this model, core and data release. The + publisher-claim basis is recorded and warned about — it is the data + publisher's assertion, made good only by this repo's test suite passing + on the pinned pair. A qualified pair is never widened into a range. + + When ``core_version`` is given, the core must also be the build-time + core, be covered by ``compatible_core_packages``, or be part of the + qualified pair. A core admitted only by the qualification makes the + basis ``qualified_exact_pair`` whatever the model's own claim, so the + certificate names the pair and its ledger evidence is enforced. """ warnings: list[str] = [] if "national" not in manifest.default_datasets: @@ -267,6 +286,21 @@ def validate_release_manifest( _specifier_matches(model_version, specifier) for specifier in claim_specifiers ) + core_rests_on_qualification = core_version is not None and ( + _check_core_compatibility(manifest, core_version, qualified_pair_id, warnings) + ) + if core_rests_on_qualification: + # The publisher never claimed this core, so the model's own claim + # cannot carry the certificate: it is the exact pair that was tested, + # and the certificate must name it so its evidence is checked. + warnings.append( + f"certifying exactly {model_package}=={model_version} with " + f"policyengine-core=={core_version} on automated qualification of " + f"pair {qualified_pair_id}; the certificate stands only with a " + "promotable qualification receipt for that pair in the ledger" + ) + return BASIS_QUALIFIED_PAIR, warnings + if built_with == model_version: return BASIS_BUILT_WITH, warnings if claim_matches: @@ -277,11 +311,65 @@ def validate_release_manifest( "suite is the arbiter" ) return BASIS_PUBLISHER_CLAIM, warnings + if qualified_pair_id is not None: + if core_version is None: + raise CertificationError( + "A qualified exact pair names its core version; pass core_version." + ) + warnings.append( + f"certifying exactly {model_package}=={model_version} with " + f"policyengine-core=={core_version} on automated qualification of " + f"pair {qualified_pair_id}; the certificate stands only with a " + "promotable qualification receipt for that pair in the ledger" + ) + return BASIS_QUALIFIED_PAIR, warnings raise CertificationError( f"{model_package} {model_version} matches neither the build-time " f"model ({built_with or 'unrecorded'}) nor any publisher " f"compatibility claim {claim_specifiers or '(none declared)'}; " - "a new data build or a published compatibility claim is required." + "a new data build, a published compatibility claim or an automated " + "qualification of the exact pair is required." + ) + + +def _check_core_compatibility( + manifest: DataReleaseManifest, + core_version: str, + qualified_pair_id: Optional[str], + warnings: list[str], +) -> bool: + """The core must be the build core, publisher-claimed, or qualified. + + Returns whether the core is admitted only by the qualification, in which + case the certificate's basis must be the qualified exact pair. + """ + + build = manifest.build + built_with_core = ( + build.built_with_core_package.version + if build is not None and build.built_with_core_package is not None + else None + ) + core_claims = [ + package.specifier + for package in manifest.compatible_core_packages + if package.name == "policyengine-core" + ] + if built_with_core == core_version or any( + _specifier_matches(core_version, specifier) for specifier in core_claims + ): + return False + if qualified_pair_id is not None: + warnings.append( + f"policyengine-core {core_version} is outside the publisher's core " + f"claims {core_claims or '(none declared)'}; covered by qualification " + f"of pair {qualified_pair_id}" + ) + return True + raise CertificationError( + f"policyengine-core {core_version} matches neither the build-time core " + f"({built_with_core or 'unrecorded'}) nor any publisher core claim " + f"{core_claims or '(none declared)'}." ) @@ -591,6 +679,8 @@ def build_country_manifest_payload( model_version: str, model_wheel: dict, compatibility_basis: str = BASIS_BUILT_WITH, + core_version: Optional[str] = None, + qualified_pair_id: Optional[str] = None, ) -> dict: """Map the data release manifest to the vendored country manifest.""" default_dataset = manifest.default_datasets["national"] @@ -651,6 +741,13 @@ def build_country_manifest_payload( "certified_for_model_version": model_version, "certified_by": CERTIFIED_BY, } + if core_version is not None: + certification["certified_for_core_version"] = core_version + if qualified_pair_id is not None: + certification["qualification_pair_id"] = qualified_pair_id + certification["qualification_ledger_url"] = ( + f"{QUALIFICATION_LEDGER_URL}/{country}/{qualified_pair_id}" + ) certified_artifact: dict = { "data_package": { "name": manifest.data_package.name, @@ -747,6 +844,8 @@ def certify( regional_manifest_uri: Optional[str] = None, regional_artifact_prefix: str = "states/", regional_path_template: str = "states/{state_code}.h5", + core_version: Optional[str] = None, + qualified_pair_id: Optional[str] = None, ) -> tuple[dict, str, list[str]]: raise NotImplementedError @@ -766,6 +865,8 @@ def certify( regional_manifest_uri: Optional[str] = None, regional_artifact_prefix: str = "states/", regional_path_template: str = "states/{state_code}.h5", + core_version: Optional[str] = None, + qualified_pair_id: Optional[str] = None, ) -> tuple[dict, str, list[str]]: raise CertificationError( "Legacy data-producer certification updates are not implemented in " @@ -788,6 +889,8 @@ def certify( regional_manifest_uri: Optional[str] = None, regional_artifact_prefix: str = "states/", regional_path_template: str = "states/{state_code}.h5", + core_version: Optional[str] = None, + qualified_pair_id: Optional[str] = None, ) -> tuple[dict, str, list[str]]: manifest, manifest_sha256, uri_parts = fetch_release_manifest( manifest_uri, token=token @@ -810,7 +913,11 @@ def certify( path_template=regional_path_template, ) compatibility_basis, warnings = validate_release_manifest( - manifest, model_package, model_version + manifest, + model_package, + model_version, + core_version=core_version, + qualified_pair_id=qualified_pair_id, ) for filename in required_supplemental_release_files( @@ -838,6 +945,12 @@ def certify( model_version=model_version, model_wheel=model_wheel or {}, compatibility_basis=compatibility_basis, + core_version=core_version, + qualified_pair_id=( + qualified_pair_id + if compatibility_basis == BASIS_QUALIFIED_PAIR + else None + ), ) if check_artifacts and should_validate_vendored_artifacts( country, manifest, uri_parts @@ -898,7 +1011,17 @@ def certify_data_release( regional_manifest_uri: Optional[str] = None, regional_artifact_prefix: str = "states/", regional_path_template: str = "states/{state_code}.h5", + core_version: Optional[str] = None, + qualified_pair_id: Optional[str] = None, ) -> CertificationResult: + """Certify a data release into the bundle manifest. + + ``core_version`` adds the core compatibility check and pins the core in + the bundle's ``packages``. ``qualified_pair_id`` admits the exact model, + core and data release when neither the build-time model nor a publisher + claim covers them; the certificate then names the pair, whose promotable + qualification receipt must be in the ledger. + """ if country not in COUNTRY_MODEL_PACKAGES: raise CertificationError(f"Unknown country {country!r}.") model_package = COUNTRY_MODEL_PACKAGES[country] @@ -915,6 +1038,8 @@ def certify_data_release( regional_manifest_uri=regional_manifest_uri, regional_artifact_prefix=regional_artifact_prefix, regional_path_template=regional_path_template, + core_version=core_version, + qualified_pair_id=qualified_pair_id, ) bundle_path = bundle_path or default_bundle_source_path() bundle = json.loads(bundle_path.read_text()) @@ -924,6 +1049,8 @@ def certify_data_release( ) if model_package in bundle.get("packages", {}): bundle["packages"][model_package]["version"] = model_version + if core_version is not None and "policyengine-core" in bundle.get("packages", {}): + bundle["packages"]["policyengine-core"]["version"] = core_version bundle_path.write_text(json.dumps(bundle, indent=2, sort_keys=True) + "\n") return CertificationResult( diff --git a/src/policyengine/provenance/manifest.py b/src/policyengine/provenance/manifest.py index 0995c0d5..05d062fe 100644 --- a/src/policyengine/provenance/manifest.py +++ b/src/policyengine/provenance/manifest.py @@ -155,6 +155,16 @@ class DataCertification(BaseModel): built_with_model_git_sha: Optional[str] = None data_build_fingerprint: Optional[str] = None certified_by: Optional[str] = None + certified_for_core_version: Optional[str] = None + qualification_pair_id: Optional[str] = None + qualification_ledger_url: Optional[str] = None + + +#: Bases whose runtime model legitimately differs from the data's build-time +#: model, so the build fingerprint is not expected to match. +CLAIM_BASED_COMPATIBILITY = frozenset( + {"legacy_compatible_model_package", "qualified_exact_pair"} +) class CertifiedDataArtifact(BaseModel): @@ -392,10 +402,32 @@ def _specifier_matches(version: str, specifier: str) -> bool: return False +def _runtime_core_is_certified( + certification: DataCertification, runtime_core_version: Optional[str] +) -> bool: + """Whether the running core is the one an exact-pair certificate tested. + + Other bases make no claim about the core at run time. An exact pair was + tested with one core, so it holds only for that core. + """ + + if certification.compatibility_basis != "qualified_exact_pair": + return True + if runtime_core_version is None: + from importlib import metadata + + try: + runtime_core_version = metadata.version("policyengine-core") + except metadata.PackageNotFoundError: + return False + return certification.certified_for_core_version == runtime_core_version + + def certify_data_release_compatibility( country_id: str, runtime_model_version: str, runtime_data_build_fingerprint: Optional[str] = None, + runtime_core_version: Optional[str] = None, ) -> DataCertification: country_manifest = get_release_manifest(country_id) try: @@ -407,7 +439,7 @@ def certify_data_release_compatibility( runtime_data_build_fingerprint is not None and bundled_certification.data_build_fingerprint is not None and bundled_certification.compatibility_basis - != "legacy_compatible_model_package" + not in CLAIM_BASED_COMPATIBILITY and runtime_data_build_fingerprint != bundled_certification.data_build_fingerprint ): @@ -418,6 +450,9 @@ def certify_data_release_compatibility( if ( bundled_certification.certified_for_model_version == runtime_model_version + and _runtime_core_is_certified( + bundled_certification, runtime_core_version + ) ): return bundled_certification return DataCertification( @@ -434,6 +469,43 @@ def certify_data_release_compatibility( certified_by=bundled_certification.certified_by, ) raise exc + + # A remote build-model, fingerprint or publisher claim cannot broaden an + # exact-pair certificate beyond the model, core and recorded data build + # that qualification tested. Check them before any acceptance path returns. + bundled_certification = country_manifest.certification + remote_build_id = ( + data_release_manifest.build.build_id + if data_release_manifest.build is not None + else None + ) + if ( + bundled_certification is not None + and bundled_certification.compatibility_basis == "qualified_exact_pair" + ): + if bundled_certification.certified_for_model_version != runtime_model_version: + raise ValueError( + "Data release manifest is not certified for the runtime model " + f"version {runtime_model_version} in country '{country_id}'; " + "the exact pair requires " + f"{bundled_certification.certified_for_model_version}." + ) + if not _runtime_core_is_certified(bundled_certification, runtime_core_version): + raise ValueError( + "Data release manifest is not certified for the runtime core " + f"version {runtime_core_version} in country '{country_id}'; " + "the exact pair requires " + f"{bundled_certification.certified_for_core_version}." + ) + if ( + bundled_certification.data_build_id is not None + and bundled_certification.data_build_id != remote_build_id + ): + raise ValueError( + "Data release manifest is not certified for the remote data " + f"build {remote_build_id} in country '{country_id}'; the exact " + f"pair requires {bundled_certification.data_build_id}." + ) built_with_model = ( data_release_manifest.build.built_with_model_package if data_release_manifest.build is not None @@ -514,6 +586,20 @@ def certify_data_release_compatibility( ), ) + # An automated qualification of exactly this model and data build: the + # bundled certificate names the pair, and the ledger evidence for it is + # enforced when the bundle is built (tests/test_certification_evidence.py). + bundled_certification = country_manifest.certification + if ( + bundled_certification is not None + and bundled_certification.compatibility_basis == "qualified_exact_pair" + and bundled_certification.certified_for_model_version == runtime_model_version + and bundled_certification.qualification_pair_id is not None + and bundled_certification.data_build_id == remote_build_id + and _runtime_core_is_certified(bundled_certification, runtime_core_version) + ): + return bundled_certification + raise ValueError( "Data release manifest is not certified for the runtime model version " f"{runtime_model_version} in country '{country_id}'." diff --git a/src/policyengine/provenance/qualification.py b/src/policyengine/provenance/qualification.py new file mode 100644 index 00000000..90b6085f --- /dev/null +++ b/src/policyengine/provenance/qualification.py @@ -0,0 +1,1033 @@ +"""Candidate qualification contracts and pure decision rules. + +A *candidate* is a country model wheel, its core wheel and a data release +that are not yet certified together. Qualification: + +1. prepares a policyengine.py commit whose packaged manifest, pins, lock and + TRO certify the pair (the candidate commit, which is also what promotion + merges); +2. runs mandatory local gates on that commit (:data:`REQUIRED_GATES`); +3. sends the replay corpus owner (policyengine-scorecard) an immutable + :class:`ReplayQualificationRequest`, polls its :class:`ReplayStatus` and + checks the :class:`ReplayReceipt` against the request + (:func:`evaluate_replay_receipt`); +4. records a :class:`QualificationReceipt` whose verdict is a pure function of + the gate results (:func:`qualification_verdict`). + +Promotion then picks the newest promotable pair that strictly advances the +certified pair (:func:`select_promotion_target`), so a stale completion +cannot displace a newer accepted candidate. + +The replay request, status and receipt mirror the JSON Schemas published by +policyengine-scorecard (vendored, unchanged, in +``policyengine/data/schemas/vendor/policyengine-scorecard/``). Their digests +are plain RFC 8785 over the document as sent, nulls included, which is what +the scorecard computes. W4's own receipts use the receipt-v1 rule of +:func:`policyengine.provenance.receipt.content_sha256` (nulls dropped). +""" + +from __future__ import annotations + +from collections.abc import Iterable, Mapping, Sequence +from typing import Any, ClassVar, Literal, Optional + +from packaging.version import Version +from pydantic import ( + BaseModel, + ConfigDict, + Field, + PrivateAttr, + field_validator, + model_validator, +) + +from policyengine.provenance.canonical import canonical_sha256 +from policyengine.provenance.manifest import ArtifactPathReference +from policyengine.provenance.receipt import ( + GIT_SHA_PATTERN, + SHA256_PATTERN, + _Contract, + content_sha256, +) + +__all__ = [ + "BLOCKING_REPLAY_FLAGS", + "REQUIRED_GATES", + "REVIEW_REPLAY_FLAGS", + "CandidatePair", + "GateResult", + "QualificationHarness", + "QualificationReceipt", + "ReplayQualificationRequest", + "ReplayReceipt", + "ReplayStatus", + "ReviewAcceptance", + "dominates", + "evaluate_replay_receipt", + "qualification_verdict", + "select_promotion_target", +] + +# --- Gate results and verdicts ------------------------------------------ + +GateStatus = Literal["pass", "needs_review", "unavailable", "fail", "error"] +Verdict = Literal["qualified", "needs_review", "incomplete", "rejected"] + +#: Every mandatory gate. A gate has no "skipped" status: a gate whose harness +#: is missing reports ``unavailable`` (the candidate is then incomplete, not +#: promotable), a gate that crashed while evaluating the candidate reports +#: ``error`` (the candidate is rejected), and a gate that never reported +#: leaves the qualification incomplete. +REQUIRED_GATES: tuple[str, ...] = ( + "installed_versions", + "wheel_hashes", + "core_compatibility", + "data_access", + "dataset_bytes", + "supported_years", + "budget_inputs", + "reform_contract", + "metrics_review", + "reform_responsiveness", + "replay", +) + +_SEVERITY: dict[str, int] = { + "pass": 0, + "needs_review": 1, + "unavailable": 2, + "fail": 3, + "error": 3, +} + +#: Replay diff flags that block promotion whatever their size. Proposed +#: policy pending cos d1344; numerical and support flags need review. +BLOCKING_REPLAY_FLAGS: frozenset[str] = frozenset( + {"structural", "coverage", "mapping", "inertness", "construction"} +) +REVIEW_REPLAY_FLAGS: frozenset[str] = frozenset({"numerical", "support"}) + + +class GateResult(_Contract): + """One mandatory qualification gate.""" + + gate_id: str + status: GateStatus + summary: str = Field(min_length=1) + evidence_sha256: Optional[str] = Field( + default=None, + pattern=SHA256_PATTERN, + description="content_sha256 of the evidence JSON stored with the receipt.", + ) + details: dict[str, Any] = Field(default_factory=dict) + + +def qualification_verdict(gates: Iterable[GateResult]) -> Verdict: + """The verdict a set of gate results determines. + + - ``rejected`` if any gate failed or errored; + - otherwise ``incomplete`` if any gate is unavailable or a required gate + did not report; + - otherwise ``needs_review`` if any gate needs review; + - otherwise ``qualified``. + + Making any gate worse never makes the verdict better. + """ + + gates = list(gates) + worst = max((_SEVERITY[gate.status] for gate in gates), default=0) + if worst >= _SEVERITY["fail"]: + return "rejected" + reported = {gate.gate_id for gate in gates} + if worst == _SEVERITY["unavailable"] or any( + gate_id not in reported for gate_id in REQUIRED_GATES + ): + return "incomplete" + if worst == _SEVERITY["needs_review"]: + return "needs_review" + return "qualified" + + +# --- What is qualified ---------------------------------------------------- + + +class PinnedWheel(_Contract): + name: str + version: str + wheel_sha256: str = Field(pattern=SHA256_PATTERN) + + +class PinnedData(_Contract): + data_package: str + release_version: str + build_id: str + repo_id: str + repo_type: Literal["model", "dataset"] + release_manifest_path: str + release_manifest_revision: str + release_manifest_sha256: str = Field(pattern=SHA256_PATTERN) + dataset: str + dataset_path: str + dataset_revision: str + dataset_sha256: str = Field(pattern=SHA256_PATTERN) + size_bytes: int = Field(ge=1) + + +class CandidatePair(_Contract): + """The model, core and data that would be certified together. + + ``pair_id`` (its receipt-v1 digest) does not depend on the policyengine.py + commit, so the candidate commit's manifest can name the pair it certifies. + """ + + country_id: Literal["uk"] + model: PinnedWheel + core: PinnedWheel + data: PinnedData + + @property + def pair_id(self) -> str: + return content_sha256(self.canonical_payload()) + + @property + def order_key(self) -> tuple[str, str, str]: + """(model version, data release, core version): what promotion orders.""" + + return (self.model.version, self.data.release_version, self.core.version) + + +class QualificationHarness(_Contract): + """The policyengine.py commit that carried and ran the qualification.""" + + policyengine_version: str + git_commit: str = Field(pattern=GIT_SHA_PATTERN) + packaged_bundle_manifest_sha256: str = Field(pattern=SHA256_PATTERN) + + +class IssuedBy(_Contract): + repository: str = Field(pattern=r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") + workflow_run_id: int = Field(ge=1) + run_attempt: int = Field(default=1, ge=1) + run_url: str + + +class PreviousCertificate(_Contract): + """The certified pair a candidate would replace.""" + + bundle_version: str + model_version: str + core_version: str + data_release_version: str + dataset_sha256: str = Field(pattern=SHA256_PATTERN) + certificate_sha256: str = Field(pattern=SHA256_PATTERN) + qualification_pair_id: Optional[str] = Field(default=None, pattern=SHA256_PATTERN) + model_wheel_sha256: Optional[str] = Field(default=None, pattern=SHA256_PATTERN) + core_wheel_sha256: Optional[str] = Field(default=None, pattern=SHA256_PATTERN) + dataset_reference: Optional[ArtifactPathReference] = None + dataset_name: Optional[str] = None + release_manifest_reference: Optional[ArtifactPathReference] = None + + +class ReplayReference(_Contract): + request_id: str = Field(pattern=SHA256_PATTERN) + receipt_sha256: Optional[str] = Field(default=None, pattern=SHA256_PATTERN) + receipt_commit: Optional[str] = Field(default=None, pattern=GIT_SHA_PATTERN) + + +class ReviewAcceptance(_Contract): + """A reviewer's acceptance of one exact needs-review receipt.""" + + reviewer: str = Field(min_length=1) + accepted_at: str + accepted_receipt_sha256: str = Field( + pattern=SHA256_PATTERN, + description="unreviewed_sha256 of the receipt being accepted.", + ) + explanation: str = Field(min_length=1) + + +class QualificationReceipt(_Contract): + """The qualification outcome recorded in the ledger for one candidate.""" + + SCHEMA_ID: ClassVar[str] = ( + "https://policyengine.org/schemas/candidate_qualification_receipt.v1.json" + ) + + schema_version: Literal[1] + receipt_type: Literal["policyengine.candidate_qualification_receipt"] + pair_id: str = Field(pattern=SHA256_PATTERN) + pair: CandidatePair + harness: QualificationHarness + previous_certificate: Optional[PreviousCertificate] = None + replay: Optional[ReplayReference] = None + gates: list[GateResult] + verdict: Verdict + review: Optional[ReviewAcceptance] = None + issued_by: IssuedBy + completed_at: str = Field(description="ISO 8601 UTC, e.g. 2026-10-15T09:30:00Z.") + + @model_validator(mode="after") + def _consistent(self) -> QualificationReceipt: + if self.pair_id != self.pair.pair_id: + raise ValueError("pair_id is not the digest of pair.") + ids = [gate.gate_id for gate in self.gates] + if len(ids) != len(set(ids)): + raise ValueError("Each gate reports at most once.") + expected = qualification_verdict(self.gates) + if self.verdict != expected: + raise ValueError( + f"verdict {self.verdict!r} does not follow from the gates ({expected!r})." + ) + if self.review is not None and self.verdict != "needs_review": + raise ValueError("Only a needs-review verdict can be accepted on review.") + return self + + @property + def unreviewed_sha256(self) -> str: + """Digest of the receipt without ``review``: what a review accepts.""" + + payload = self.canonical_payload() + payload.pop("review", None) + return content_sha256(payload) + + @property + def receipt_sha256(self) -> str: + return content_sha256(self.canonical_payload()) + + @property + def promotable(self) -> bool: + """Qualified, or needs review with an acceptance of exactly this receipt.""" + + if self.verdict == "qualified": + return True + return ( + self.verdict == "needs_review" + and self.review is not None + and self.review.accepted_receipt_sha256 == self.unreviewed_sha256 + ) + + +# --- Promotion order ------------------------------------------------------ + + +def _versions(key: Sequence[str]) -> tuple[Version, ...]: + return tuple(Version(part) for part in key) + + +def dominates(newer: Sequence[str], older: Sequence[str]) -> bool: + """``newer`` is at least ``older`` in every component and differs from it.""" + + a, b = _versions(newer), _versions(older) + if len(a) != len(b): + raise ValueError("Order keys must have the same length.") + return a != b and all(x >= y for x, y in zip(a, b)) + + +def select_promotion_target( + receipts: Iterable[QualificationReceipt], + current: Optional[Sequence[str]], +) -> Optional[QualificationReceipt]: + """The promotable candidate to promote next, or ``None``. + + Eligible receipts are promotable and strictly advance ``current`` (model + version, data release, core version). Among them the target is the one + whose pair dominates every other eligible pair. If eligible pairs have no + single newest member (a newer model with an older data release, say), + nothing is promoted automatically. The result depends only on the set of + receipts, never on the order they arrive in. + """ + + eligible: dict[tuple[str, ...], QualificationReceipt] = {} + for receipt in receipts: + if not receipt.promotable: + continue + key = tuple(receipt.pair.order_key) + if current is not None and not dominates(key, current): + continue + incumbent = eligible.get(key) + if incumbent is None or (receipt.completed_at, receipt.receipt_sha256) > ( + incumbent.completed_at, + incumbent.receipt_sha256, + ): + eligible[key] = receipt + keys = list(eligible) + for key in keys: + if all(other == key or dominates(key, other) for other in keys): + return eligible[key] + return None + + +# --- Replay interface (mirrors policyengine-scorecard schemas v1) --------- + + +class _Mirror(BaseModel): + """Closed, immutable mirror of a scorecard JSON Schema object.""" + + model_config = ConfigDict(frozen=True, extra="forbid", populate_by_name=True) + + def document(self) -> dict[str, Any]: + """The JSON document exactly as the scorecard schema spells it.""" + + return self.model_dump(mode="json", by_alias=True, exclude_none=True) + + +class _Open(_Mirror): + model_config = ConfigDict(frozen=True, extra="allow", populate_by_name=True) + + +_SHA1 = GIT_SHA_PATTERN +_VERSION = r"^[0-9]+(\.[0-9]+)*([a-z0-9.+-]*)$" + + +class GitSource(_Mirror): + repository: Literal["PolicyEngine/policyengine.py"] + commit: str = Field(pattern=_SHA1) + + +class PypiSource(_Mirror): + wheel_sha256: str = Field(pattern=SHA256_PATTERN) + + +class WrapperSource(_Mirror): + git: Optional[GitSource] = None + pypi: Optional[PypiSource] = None + + @model_validator(mode="after") + def _one_source(self) -> WrapperSource: + if (self.git is None) == (self.pypi is None): + raise ValueError("The wrapper source is exactly one of git or pypi.") + return self + + +class RequestWrapper(_Mirror): + version: str = Field(pattern=_VERSION) + source: WrapperSource + packaged_bundle_manifest_sha256: str = Field(pattern=SHA256_PATTERN) + + +class RequestPackage(_Mirror): + version: str = Field(pattern=_VERSION) + wheel_sha256: str = Field(pattern=SHA256_PATTERN) + + +class RequestData(_Mirror): + build_id: str = Field(min_length=1) + dataset: str = Field(min_length=1) + repo_id: str = Field(pattern=r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") + repo_type: Literal["model", "dataset"] + revision: str = Field(min_length=1) + path: str = Field(min_length=1, pattern=r"^[^/][^\\]*$") + sha256: str = Field(pattern=SHA256_PATTERN) + size_bytes: int = Field(ge=1) + release_manifest_sha256: str = Field(pattern=SHA256_PATTERN) + + @field_validator("path") + @classmethod + def _no_parent_segments(cls, path: str) -> str: + # The schema says ^(?!.*(^|/)\.\.(/|$))[^/][^\\]*$; pydantic's regex + # engine has no look-ahead, so the '..' rule is checked here. + if ".." in path.split("/"): + raise ValueError("data.path must not contain '..' segments.") + return path + + +class RequestCandidate(_Mirror): + policyengine: RequestWrapper + policyengine_uk: RequestPackage + policyengine_core: RequestPackage + data: RequestData + expected_release_bundle: dict[str, Optional[str]] + + @field_validator("expected_release_bundle") + @classmethod + def _required_bundle_fields( + cls, bundle: dict[str, Optional[str]] + ) -> dict[str, Optional[str]]: + for key in ( + "policyengine_version", + "model_version", + "certified_data_build_id", + "certified_data_artifact_sha256", + "default_dataset_uri", + ): + if not bundle.get(key): + raise ValueError(f"expected_release_bundle needs {key}.") + return bundle + + +class RequestedBy(_Open): + repository: str = Field(pattern=r"^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$") + workflow_run_id: Optional[int] = Field(default=None, ge=1) + run_attempt: Optional[int] = Field(default=None, ge=1) + commit: str = Field(pattern=_SHA1) + + +class ReplayQualificationRequest(_Mirror): + """``policyengine.uk-replay-qualification-request/v1``.""" + + schema_id: Literal["policyengine.uk-replay-qualification-request/v1"] = Field( + alias="schema" + ) + kind: Literal["candidate", "released_bundle"] + attempt: int = Field(ge=1) + candidate: RequestCandidate + compare_to: str = Field(min_length=1) + requested_by: RequestedBy + + @model_validator(mode="after") + def _released_bundles_come_from_pypi(self) -> ReplayQualificationRequest: + if ( + self.kind == "released_bundle" + and self.candidate.policyengine.source.pypi is None + ): + raise ValueError("A released_bundle request names the PyPI wheel.") + return self + + @property + def request_id(self) -> str: + """Plain RFC 8785 SHA-256 of the request document, as the scorecard computes.""" + + return canonical_sha256(self.document()) + + +class StatusEvent(_Mirror): + status: Literal["queued", "running", "complete", "rejected", "failed"] + at: str + run_url: str = Field( + pattern=r"^https://github\.com/PolicyEngine/policyengine-scorecard/actions/runs/[0-9]+" + ) + + +class ReplayStatus(_Mirror): + """``policyengine.uk-replay-status/v1`` (``requests//STATUS.json``).""" + + schema_id: Literal["policyengine.uk-replay-status/v1"] = Field(alias="schema") + request_id: str = Field(pattern=SHA256_PATTERN) + status: Literal["queued", "running", "complete", "rejected", "failed"] + history: list[StatusEvent] = Field(min_length=1) + receipt_sha256: Optional[str] = Field(default=None, pattern=SHA256_PATTERN) + receipt_commit: Optional[str] = Field(default=None, pattern=_SHA1) + + @model_validator(mode="after") + def _terminal_names_receipt(self) -> ReplayStatus: + if self.terminal and ( + self.receipt_sha256 is None or self.receipt_commit is None + ): + raise ValueError("A terminal status names its receipt digest and commit.") + return self + + @property + def terminal(self) -> bool: + return self.status in ("complete", "rejected", "failed") + + +class Rejection(_Mirror): + code: Literal[ + "schema_invalid", + "artifact_hash_mismatch", + "release_bundle_mismatch", + "incompatible_pair", + "data_access_failed", + "no_supported_years", + ] + detail: str = Field(min_length=1) + + +class InstalledPackage(_Mirror): + version: str = Field(min_length=1) + record_sha256: str = Field(pattern=SHA256_PATTERN) + wheel_sha256: Optional[str] = Field(default=None, pattern=SHA256_PATTERN) + + +class ExecutedPackages(_Mirror): + policyengine: InstalledPackage + policyengine_uk: InstalledPackage = Field(alias="policyengine-uk") + policyengine_core: InstalledPackage = Field(alias="policyengine-core") + + +class Execution(_Mirror): + packages: ExecutedPackages + packaged_bundle_manifest_sha256: str = Field(pattern=SHA256_PATTERN) + release_bundle: dict[str, Optional[str]] + dataset_sha256: str = Field(pattern=SHA256_PATTERN) + requirements_sha256: str = Field(pattern=SHA256_PATTERN) + python: str = Field(pattern=r"^3\.[0-9]+\.[0-9]+$") + platform: str = Field(min_length=1) + runner: str = Field(min_length=1) + + +class UnsupportedYear(_Mirror): + year: int + reason: Literal["before_data_year", "after_extension_end"] + + +class InertMeasureYear(_Mirror): + measure_key: str + year: int + + +class EventResult(_Mirror): + registry_sha256: str = Field(pattern=SHA256_PATTERN) + supported_years: list[int] + unsupported_years: list[UnsupportedYear] + run_manifest_sha256: str = Field(pattern=SHA256_PATTERN) + staging_manifest_sha256: str = Field(pattern=SHA256_PATTERN) + comparison_provenance_sha256: str = Field(pattern=SHA256_PATTERN) + artifacts: int = Field(ge=0) + full_supported_grid_complete: bool + inert_measure_years: list[InertMeasureYear] + + +class Corpus(_Mirror): + corpus_version: str = Field(min_length=1) + events: dict[str, EventResult] = Field(min_length=1) + + +class IntegrityFailure(_Mirror): + code: str = Field(min_length=1) + detail: str = Field(min_length=1) + event: Optional[str] = None + measure_key: Optional[str] = None + year: Optional[int] = None + + +class Integrity(_Mirror): + status: Literal["complete", "incomplete", "failed", "not_run"] + failures: list[IntegrityFailure] + + +FlagKind = Literal[ + "numerical", + "structural", + "coverage", + "mapping", + "support", + "inertness", + "construction", +] + + +class FlagCounts(_Mirror): + numerical: int = Field(ge=0) + structural: int = Field(ge=0) + coverage: int = Field(ge=0) + mapping: int = Field(ge=0) + support: int = Field(ge=0) + inertness: int = Field(ge=0) + construction: int = Field(ge=0) + + +class FlaggedRow(_Mirror): + event: str = Field(min_length=1) + measure_key: str = Field(min_length=1) + year: int + level: Literal["source_row", "measure_year", "head"] + key: str = Field(min_length=1) + flags: list[FlagKind] = Field(min_length=1) + base_gbp: Optional[float] = None + head_gbp: Optional[float] = None + + +class Diff(_Mirror): + base_variant_id: str = Field(min_length=1) + diff_sha256: str = Field(pattern=SHA256_PATTERN) + flag_counts: FlagCounts + flagged_rows: list[FlaggedRow] + + +class ResultsLocation(_Mirror): + repository: Literal["PolicyEngine/policyengine-scorecard"] + ref: str = Field(min_length=1) + commit: Optional[str] = Field(default=None, pattern=_SHA1) + path: str = Field(min_length=1) + manifest_sha256: Optional[str] = Field( + default=None, + pattern=SHA256_PATTERN, + description="Digest of NAMESPACE_MANIFEST.json for the sealed result namespace.", + ) + + +class ReplayReceipt(_Mirror): + """``policyengine.uk-replay-receipt/v1`` (``requests//REPLAY_RECEIPT.json``). + + Nullable members are kept as ``null`` in :meth:`document`, because the + scorecard's ``receipt_sha256`` covers them. + """ + + schema_id: Literal["policyengine.uk-replay-receipt/v1"] = Field(alias="schema") + request_id: str = Field(pattern=SHA256_PATTERN) + status: Literal["complete", "rejected", "failed"] + rejection: Optional[Rejection] + variant_id: Optional[str] + variant_identity_sha256: Optional[str] = Field(pattern=SHA256_PATTERN) + execution: Optional[Execution] + corpus: Optional[Corpus] + integrity: Integrity + diff: Optional[Diff] + results: ResultsLocation + receipt_sha256: str = Field(pattern=SHA256_PATTERN) + + _received: Optional[dict[str, Any]] = PrivateAttr(default=None) + + @classmethod + def from_document(cls, document: Mapping[str, Any]) -> ReplayReceipt: + """Parse a received receipt, keeping its bytes for digest checks.""" + + receipt = cls.model_validate(dict(document)) + receipt._received = dict(document) + return receipt + + def document(self) -> dict[str, Any]: + """The received document if there is one, else this model's spelling.""" + + if self._received is not None: + return dict(self._received) + top = self.model_dump(mode="json", by_alias=True, exclude_none=False) + for key in ("rejection", "execution", "corpus", "diff", "results", "integrity"): + value = getattr(self, key) + top[key] = value.document() if value is not None else None + return top + + @property + def computed_receipt_sha256(self) -> str: + """Plain RFC 8785 SHA-256 of the receipt without ``receipt_sha256``.""" + + document = self.document() + document.pop("receipt_sha256", None) + return canonical_sha256(document) + + @model_validator(mode="after") + def _status_shape(self) -> ReplayReceipt: + if self.status == "rejected": + if ( + self.rejection is None + or self.corpus is not None + or self.diff is not None + or self.variant_id is not None + or self.variant_identity_sha256 is not None + or self.execution is not None + or self.integrity.status != "not_run" + ): + raise ValueError( + "A rejected receipt has a rejection, no variant, execution, " + "corpus or diff, and integrity not_run." + ) + elif self.status == "complete": + if ( + self.rejection is not None + or self.variant_id is None + or self.variant_identity_sha256 is None + or self.execution is None + or self.results.manifest_sha256 is None + ): + raise ValueError( + "A complete receipt has a variant and its identity, an " + "execution and a sealed results manifest." + ) + # A failed receipt can come from a step before the candidate's + # identity was known (for example a download error). + elif self.rejection is not None or self.integrity.status != "failed": + raise ValueError( + "A failed receipt has no rejection and a failed integrity." + ) + if self.status == "complete" and ( + self.integrity.status not in ("complete", "incomplete") + or self.corpus is None + ): + raise ValueError("A complete receipt has a corpus and a run integrity.") + return self + + +def evaluate_replay_receipt( + request: ReplayQualificationRequest, + status: ReplayStatus, + receipt: ReplayReceipt, +) -> GateResult: + """The ``replay`` gate for a terminal replay status and its receipt. + + The receipt must answer this exact request, be the bytes the status + names, and report an execution of exactly the requested artifacts. + Rejected or failed replays, incomplete runs and blocking flags fail the + gate; numerical and support flags need review. Unsupported years are + reported, never failed on their own. + + The receipt is read as a whole, not by its summary counters: a flag + counts when either ``flag_counts`` or a flagged row carries it, and a + receipt that contradicts itself fails (failures listed under a complete + integrity, more flagged rows of a kind than its count, supported years + with no artifacts, nothing replayed at all) or contradicts the request + (a comparison base other than the ``compare_to`` asked for). Inert + measure-years with no inertness flag need review. + """ + + request_id = request.request_id + problems: list[str] = [] + if status.request_id != request_id or receipt.request_id != request_id: + problems.append("status or receipt answers a different request") + if not status.terminal: + problems.append(f"replay is still {status.status}") + if status.status != receipt.status and status.terminal: + problems.append("status and receipt disagree on the outcome") + if receipt.computed_receipt_sha256 != receipt.receipt_sha256: + problems.append("receipt_sha256 does not match the receipt bytes") + if status.receipt_sha256 != receipt.receipt_sha256: + problems.append("status names different receipt bytes") + + details: dict[str, Any] = { + "request_id": request_id, + "receipt_sha256": receipt.receipt_sha256, + "receipt_commit": status.receipt_commit, + "status": receipt.status, + "integrity": receipt.integrity.status, + } + if problems: + return GateResult( + gate_id="replay", + status="fail", + summary="Replay receipt is not bound to this request: " + + "; ".join(problems), + details={**details, "binding_problems": problems}, + ) + if receipt.status == "rejected": + assert receipt.rejection is not None + return GateResult( + gate_id="replay", + status="fail", + summary=f"Replay rejected the candidate ({receipt.rejection.code}): {receipt.rejection.detail}", + details={**details, "rejection": receipt.rejection.document()}, + ) + + execution = receipt.execution + if execution is None: + codes = sorted({failure.code for failure in receipt.integrity.failures}) + return GateResult( + gate_id="replay", + status="fail", + summary="Replay failed before the candidate was identified: " + + (", ".join(codes) or "no failure codes"), + details={ + **details, + "integrity_failures": [ + failure.document() for failure in receipt.integrity.failures + ], + }, + ) + candidate = request.candidate + mismatches: list[str] = [] + packages = { + "policyengine": ( + execution.packages.policyengine, + candidate.policyengine.version, + ( + candidate.policyengine.source.pypi.wheel_sha256 + if candidate.policyengine.source.pypi is not None + else None + ), + ), + "policyengine-uk": ( + execution.packages.policyengine_uk, + candidate.policyengine_uk.version, + candidate.policyengine_uk.wheel_sha256, + ), + "policyengine-core": ( + execution.packages.policyengine_core, + candidate.policyengine_core.version, + candidate.policyengine_core.wheel_sha256, + ), + } + for name, (installed, version, wheel) in packages.items(): + if installed.version != version: + mismatches.append(f"{name} {installed.version} ran, not {version}") + if wheel is not None and installed.wheel_sha256 != wheel: + mismatches.append( + f"{name} wheel hash is missing from the receipt" + if installed.wheel_sha256 is None + else f"{name} wheel hash differs from the request" + ) + if ( + execution.packaged_bundle_manifest_sha256 + != candidate.policyengine.packaged_bundle_manifest_sha256 + ): + mismatches.append("packaged bundle manifest differs from the request") + if execution.dataset_sha256 != candidate.data.sha256: + mismatches.append("dataset bytes differ from the request") + if execution.release_bundle != candidate.expected_release_bundle: + mismatches.append("release bundle differs from the expected release bundle") + if mismatches: + return GateResult( + gate_id="replay", + status="fail", + summary="Replay ran something other than the candidate: " + + "; ".join(mismatches), + details={**details, "execution_mismatches": mismatches}, + ) + + if receipt.status == "failed" or receipt.integrity.status == "failed": + codes = sorted({failure.code for failure in receipt.integrity.failures}) + return GateResult( + gate_id="replay", + status="fail", + summary="Replay integrity failed: " + + (", ".join(codes) or "no failure codes"), + details={ + **details, + "integrity_failures": [ + f.document() for f in receipt.integrity.failures + ], + }, + ) + corpus = receipt.corpus + assert corpus is not None + incomplete_events = sorted( + name + for name, event in corpus.events.items() + if not event.full_supported_grid_complete + ) + unsupported = sorted( + (name, item.year, item.reason) + for name, event in corpus.events.items() + for item in event.unsupported_years + ) + inert = sorted( + (name, item.measure_key, item.year) + for name, event in corpus.events.items() + for item in event.inert_measure_years + ) + details.update( + { + "corpus_version": corpus.corpus_version, + "events": sorted(corpus.events), + "unsupported_years": [list(item) for item in unsupported], + "inert_measure_years": [list(item) for item in inert], + } + ) + if receipt.integrity.status != "complete" or incomplete_events: + return GateResult( + gate_id="replay", + status="fail", + summary=( + "Replay did not complete every supported measure-year" + + (f" ({', '.join(incomplete_events)})" if incomplete_events else "") + + "; mandatory gates take no partial evidence." + ), + details={**details, "incomplete_events": incomplete_events}, + ) + # A receipt whose parts disagree is not evidence of anything. + contradictions: list[str] = [] + if receipt.integrity.failures: + codes = sorted({failure.code for failure in receipt.integrity.failures}) + contradictions.append( + f"integrity is complete but lists failures ({', '.join(codes)})" + ) + replayed = 0 + for name, event in sorted(corpus.events.items()): + replayed += event.artifacts + if event.supported_years and event.artifacts == 0: + contradictions.append(f"{name} has supported years but no artifacts") + if not event.supported_years and not event.unsupported_years: + contradictions.append(f"{name} names no years at all") + if replayed == 0: + contradictions.append("no measure-year was replayed") + counts = receipt.diff.flag_counts.document() if receipt.diff is not None else {} + rows = receipt.diff.flagged_rows if receipt.diff is not None else [] + row_counts: dict[str, int] = {} + for row in rows: + for kind in row.flags: + row_counts[kind] = row_counts.get(kind, 0) + 1 + for kind, seen in sorted(row_counts.items()): + if counts.get(kind, 0) < seen: + contradictions.append( + f"{seen} flagged row(s) carry {kind} but flag_counts says " + f"{counts.get(kind, 0)}" + ) + details["flag_counts"] = counts + details["flagged_row_counts"] = row_counts + if receipt.diff is not None: + details["base_variant_id"] = receipt.diff.base_variant_id + details["diff_sha256"] = receipt.diff.diff_sha256 + if request.compare_to != "latest_certified": + base = receipt.diff.base_variant_id if receipt.diff is not None else None + if base != request.compare_to: + contradictions.append( + f"the request asked for a comparison with {request.compare_to}, " + f"but the receipt compares with {base}" + ) + if contradictions: + return GateResult( + gate_id="replay", + status="fail", + summary="Replay receipt contradicts itself or the request: " + + "; ".join(contradictions), + details={**details, "contradictions": contradictions}, + ) + # Flags come from every place the receipt can state them, never from the + # summary counters alone. + present = {kind for kind, n in counts.items() if n} | set(row_counts) + blocking = sorted(present & BLOCKING_REPLAY_FLAGS) + review = sorted(present & REVIEW_REPLAY_FLAGS) + + def tally(kinds: Sequence[str]) -> str: + return ", ".join( + f"{kind} {max(counts.get(kind, 0), row_counts.get(kind, 0))}" + for kind in kinds + ) + + if blocking: + return GateResult( + gate_id="replay", + status="fail", + summary="Blocking replay flags: " + tally(blocking), + details=details, + ) + if review: + return GateResult( + gate_id="replay", + status="needs_review", + summary="Replay moves need review: " + tally(review), + details=details, + ) + if inert: + # A measure that does nothing in a supported year, with no inertness + # flag against the base: unchanged from the base, but still a measure + # the model does not respond to. A person decides whether it is known. + return GateResult( + gate_id="replay", + status="needs_review", + summary=( + f"{len(inert)} replayed measure-year(s) are inert, with no " + "inertness flag against the base: " + + ", ".join(f"{m} {y}" for _, m, y in inert[:6]) + ), + details=details, + ) + if receipt.diff is None: + return GateResult( + gate_id="replay", + status="needs_review", + summary="Replay completed but reported no diff against a certified variant.", + details=details, + ) + return GateResult( + gate_id="replay", + status="pass", + summary=f"Replayed {len(corpus.events)} event(s) with no flags against {receipt.diff.base_variant_id}.", + details=details, + ) + + +def replay_gate_while_pending(status: Optional[ReplayStatus]) -> GateResult: + """Gate result for a replay that has not reached a terminal status.""" + + state = status.status if status is not None else "not yet queued" + return GateResult( + gate_id="replay", + status="unavailable", + summary=f"Replay is {state}; the candidate stays incomplete until it finishes.", + details={"status": state}, + ) + + +def mirror_from_document(model: type[_Mirror], document: Mapping[str, Any]) -> Any: + """Parse a scorecard document into its mirror model.""" + + return model.model_validate(dict(document)) diff --git a/src/policyengine/provenance/qualification_ledger.py b/src/policyengine/provenance/qualification_ledger.py new file mode 100644 index 00000000..cb1aa6b6 --- /dev/null +++ b/src/policyengine/provenance/qualification_ledger.py @@ -0,0 +1,356 @@ +"""The qualification ledger: durable evidence for every candidate attempt. + +The ledger is the orphan branch ``uk-qualification-ledger`` of +PolicyEngine/policyengine.py. Each candidate pair owns one directory, so +concurrent qualifications never write the same file:: + + // + pair.json the CandidatePair + qualification_receipt.json the latest terminal receipt for the pair + latest_attempt.json which attempt that receipt came from + attempts// + harness.json the candidate commit that ran it + gates/.json each gate's evidence + replay/request.json the request sent to the scorecard + replay/status.json the terminal status read back + replay/receipt.json the receipt bytes read back + qualification_receipt.json this attempt's receipt + +Failed and incomplete attempts are kept: the ledger never deletes evidence. +A pair's published files are derived from its attempts and are trusted only +when the published receipt is one attempt's own receipt +(:func:`published_receipt`): concurrent publishes must never yield a receipt +that mixes two attempts. +A bundle certificate with basis ``qualified_exact_pair`` must point at a pair +whose latest receipt is promotable and matches the certificate exactly +(:func:`verify_certification_evidence`). +""" + +from __future__ import annotations + +import json +from collections.abc import Callable, Iterable, Mapping +from pathlib import Path +from typing import Any, Optional + +from policyengine.provenance.qualification import QualificationReceipt + +LEDGER_REPOSITORY = "PolicyEngine/policyengine.py" +LEDGER_BRANCH = "uk-qualification-ledger" +RAW_LEDGER_URL = ( + f"https://raw.githubusercontent.com/{LEDGER_REPOSITORY}/{LEDGER_BRANCH}" +) + +__all__ = [ + "LEDGER_BRANCH", + "LEDGER_REPOSITORY", + "LedgerInconsistency", + "attempt_directory", + "fetch_ledger_receipt", + "latest_receipts", + "pair_directory", + "pending_attempts", + "latest_attempt_receipt", + "published_receipt", + "rederive_publication", + "verify_certification_evidence", + "write_json", +] + + +class LedgerInconsistency(ValueError): + """A pair's published receipt is not any one attempt's receipt.""" + + +def write_json(path: Path, value: Any) -> None: + """Write readable, deterministic JSON (digests are over the JSON value).""" + + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps(value, indent=2, sort_keys=True, ensure_ascii=False) + "\n", + encoding="utf-8", + ) + + +def read_json(path: Path) -> Any: + return json.loads(path.read_text(encoding="utf-8")) + + +def pair_directory(root: Path, country_id: str, pair_id: str) -> Path: + return root / country_id / pair_id + + +def attempt_directory( + root: Path, country_id: str, pair_id: str, attempt_key: str +) -> Path: + if "/" in attempt_key or attempt_key in ("", ".", ".."): + raise ValueError(f"Invalid attempt key {attempt_key!r}.") + return pair_directory(root, country_id, pair_id) / "attempts" / attempt_key + + +def _matches_an_attempt( + receipt: QualificationReceipt, attempt_receipts: Iterable[QualificationReceipt] +) -> bool: + """Whether ``receipt`` is one attempt's receipt, a review acceptance aside. + + ``unreviewed_sha256`` covers every field but ``review``, so a receipt that + joins one attempt's harness to another's replay or gates matches neither. + """ + + return any( + attempt.unreviewed_sha256 == receipt.unreviewed_sha256 + for attempt in attempt_receipts + ) + + +def latest_attempt_receipt( + pair_dir: Path, +) -> Optional[tuple[str, int, QualificationReceipt]]: + """The receipt a pair publishes: its latest-staged attempt's current receipt. + + Attempts are ordered by the ``sequence`` in their ``staging.json`` (0 when + absent); an attempt without a receipt yet does not count. Returns the + attempt key, its sequence and its receipt, or None if no attempt has one. + """ + + latest: Optional[tuple[int, str, Path]] = None + for path in pair_dir.glob("attempts/*/qualification_receipt.json"): + stage = path.parent / "staging.json" + sequence = int(read_json(stage)["sequence"]) if stage.exists() else 0 + candidate = (sequence, path.parent.name, path) + if latest is None or candidate[:2] > latest[:2]: + latest = candidate + if latest is None: + return None + sequence, key, path = latest + return key, sequence, QualificationReceipt.model_validate(read_json(path)) + + +def published_receipt(pair_dir: Path) -> QualificationReceipt: + """A pair's published receipt, verified against the attempts it derives from. + + It must be the current receipt of the pair's latest-staged attempt + (:func:`latest_attempt_receipt`), up to a review acceptance, which is added + afterwards. Raises :class:`LedgerInconsistency` otherwise, for instance + after a textual merge of two attempts' receipts. + """ + + receipt = QualificationReceipt.model_validate( + read_json(pair_dir / "qualification_receipt.json") + ) + latest = latest_attempt_receipt(pair_dir) + if latest is None: + raise LedgerInconsistency( + f"{pair_dir.name}: a qualification receipt is published but no " + "attempt has one" + ) + key, _, attempt = latest + if not _matches_an_attempt(receipt, [attempt]): + raise LedgerInconsistency( + f"{pair_dir.name}: the published qualification receipt " + f"({receipt.unreviewed_sha256}) is not the receipt of attempt {key}, " + "the latest staged" + ) + return receipt + + +def rederive_publication(pair_dir: Path) -> Optional[QualificationReceipt]: + """Rewrite a pair's published files from its attempts. + + Writes ``qualification_receipt.json`` (the latest-staged attempt's + receipt), ``latest_attempt.json`` and ``pair.json``. A review acceptance of + exactly that receipt is kept, from the published receipt if it validates or + from ``reviews/``, the latest acceptance first. Used after a concurrent + publish, so that git never decides these files by merging two attempts. + """ + + latest = latest_attempt_receipt(pair_dir) + if latest is None: + return None + key, sequence, receipt = latest + published = pair_dir / "qualification_receipt.json" + accepted: list[QualificationReceipt] = [] + for source in [published, *sorted(pair_dir.glob("reviews/*.json"))]: + if not source.exists(): + continue + try: + candidate = QualificationReceipt.model_validate(read_json(source)) + except ValueError: + continue + if ( + candidate.review is not None + and candidate.promotable + and candidate.unreviewed_sha256 == receipt.unreviewed_sha256 + ): + accepted.append(candidate) + if accepted: + receipt = max(accepted, key=lambda item: item.review.accepted_at) + write_json(published, receipt.canonical_payload()) + write_json( + pair_dir / "latest_attempt.json", {"attempt_key": key, "sequence": sequence} + ) + write_json(pair_dir / "pair.json", receipt.pair.canonical_payload()) + return receipt + + +def latest_receipts( + root: Path, + country_id: str = "uk", + *, + problems: Optional[list[str]] = None, +) -> list[QualificationReceipt]: + """Every pair's latest terminal receipt in a checked-out ledger. + + A pair whose published receipt is not one of its attempts' receipts is + left out, and the reason is appended to ``problems`` when given. + """ + + base = root / country_id + if not base.is_dir(): + return [] + receipts = [] + for path in sorted(base.glob("*/qualification_receipt.json")): + try: + receipts.append(published_receipt(path.parent)) + except LedgerInconsistency as error: + if problems is not None: + problems.append(str(error)) + return receipts + + +def pending_attempts(root: Path, country_id: str = "uk") -> list[Path]: + """Attempts that sent a replay request and have no receipt yet.""" + + base = root / country_id + if not base.is_dir(): + return [] + return sorted( + path.parent.parent + for path in base.glob("*/attempts/*/replay/request.json") + if not (path.parent.parent / "qualification_receipt.json").exists() + ) + + +def fetch_ledger_receipt( + country_id: str, + pair_id: str, + *, + get: Optional[Callable[..., Any]] = None, +) -> Optional[QualificationReceipt]: + """Read a pair's latest receipt from the published ledger branch. + + The receipt must be the receipt of the attempt ``latest_attempt.json`` + names (a review acceptance aside), as :func:`published_receipt` requires + of a checked-out ledger; otherwise :class:`LedgerInconsistency` is raised. + """ + + if get is None: + import requests + + get = requests.get + base = f"{RAW_LEDGER_URL}/{country_id}/{pair_id}" + response = get(f"{base}/qualification_receipt.json", timeout=30) + if response.status_code == 404: + return None + response.raise_for_status() + receipt = QualificationReceipt.model_validate_json(response.text) + pointer_response = get(f"{base}/latest_attempt.json", timeout=30) + if pointer_response.status_code == 404: + raise LedgerInconsistency( + f"{pair_id}: the published receipt names no attempt (no latest_attempt.json)" + ) + pointer_response.raise_for_status() + attempt_key = json.loads(pointer_response.text).get("attempt_key") + if not isinstance(attempt_key, str) or not attempt_key or "/" in attempt_key: + raise LedgerInconsistency(f"{pair_id}: latest_attempt.json is malformed") + attempt_response = get( + f"{base}/attempts/{attempt_key}/qualification_receipt.json", timeout=30 + ) + if attempt_response.status_code == 404: + raise LedgerInconsistency( + f"{pair_id}: attempt {attempt_key} has no qualification receipt" + ) + attempt_response.raise_for_status() + attempt = QualificationReceipt.model_validate_json(attempt_response.text) + if not _matches_an_attempt(receipt, [attempt]): + raise LedgerInconsistency( + f"{pair_id}: the published qualification receipt is not attempt " + f"{attempt_key}'s receipt" + ) + return receipt + + +def verify_certification_evidence( + record: Mapping[str, Any], + receipt: Optional[QualificationReceipt], +) -> list[str]: + """Problems with a bundle certification record's qualification evidence. + + Records on any basis other than ``qualified_exact_pair`` need no ledger + evidence and return no problems. A qualified exact pair needs a + promotable receipt for exactly the model wheel (its hash must be stated), + core, data release and dataset the record certifies, including the + default dataset's reference (repository, path, revision and bytes), which + is what the runtime resolves. + """ + + certification = record.get("certification") or {} + if certification.get("compatibility_basis") != "qualified_exact_pair": + return [] + pair_id = certification.get("qualification_pair_id") + if not pair_id: + return ["qualified_exact_pair certificate names no qualification_pair_id"] + if receipt is None: + return [f"no qualification receipt in the ledger for pair {pair_id}"] + + problems: list[str] = [] + if receipt.pair_id != pair_id: + problems.append("ledger receipt is for a different pair") + if not receipt.promotable: + problems.append( + f"ledger receipt verdict is {receipt.verdict} without an accepted review" + ) + pair = receipt.pair + model = record.get("model_package") or {} + data = record.get("data_package") or {} + artifact = record.get("certified_data_artifact") or {} + reference = (record.get("datasets") or {}).get(record.get("default_dataset")) or {} + expected: Iterable[tuple[str, Any, Any]] = ( + ( + "model version", + pair.model.version, + certification.get("certified_for_model_version"), + ), + ("model version in bundle", pair.model.version, model.get("version")), + ("model wheel sha256", pair.model.wheel_sha256, model.get("sha256")), + ( + "core version", + pair.core.version, + certification.get("certified_for_core_version"), + ), + ("data release", pair.data.release_version, data.get("version")), + ( + "release manifest revision", + pair.data.release_manifest_revision, + data.get("release_manifest_revision"), + ), + ("default dataset", pair.data.dataset, record.get("default_dataset")), + ("certified dataset", pair.data.dataset, artifact.get("dataset")), + ("dataset sha256", pair.data.dataset_sha256, artifact.get("sha256")), + # The reference the runtime actually resolves and downloads. + ("dataset reference repo", pair.data.repo_id, reference.get("repo_id")), + ("dataset reference path", pair.data.dataset_path, reference.get("path")), + ( + "dataset reference revision", + pair.data.dataset_revision, + reference.get("revision"), + ), + ("dataset reference sha256", pair.data.dataset_sha256, reference.get("sha256")), + ("data build id", pair.data.build_id, certification.get("data_build_id")), + ) + for label, qualified, certified in expected: + if qualified != certified: + problems.append( + f"{label}: qualified {qualified!r}, certified {certified!r}" + ) + return problems diff --git a/src/policyengine/provenance/uk_candidate.py b/src/policyengine/provenance/uk_candidate.py new file mode 100644 index 00000000..e77ec711 --- /dev/null +++ b/src/policyengine/provenance/uk_candidate.py @@ -0,0 +1,3291 @@ +"""Mandatory local qualification gates for a UK model/data candidate. + +Each gate returns a :class:`~policyengine.provenance.qualification.GateResult` +and an evidence dict whose ``content_sha256`` the result records. No gate has +a skip path: a gate whose harness is missing reports ``unavailable`` (the +candidate is incomplete), a gate that crashes reports ``error`` (rejected), +and a failed check reports ``fail`` (rejected). The replay gate is evaluated +separately (:func:`policyengine.provenance.qualification.evaluate_replay_receipt`). + +I/O is injected (metadata readers, HTTP getters, importers, year projectors) +so every gate is testable offline. :func:`run_candidate_gates` wires the real +implementations for the qualification workflow. +""" + +from __future__ import annotations + +import hashlib +import importlib +import inspect +import io +import json +import math +import re +import sys +import traceback +from collections import Counter +from collections.abc import Callable, Iterable, Mapping, Sequence +from importlib import metadata as importlib_metadata +from pathlib import Path +from typing import Any, Optional + +from packaging.requirements import Requirement +from packaging.specifiers import SpecifierSet +from packaging.version import Version + +from policyengine.provenance.manifest import DataReleaseManifest, _specifier_matches +from policyengine.provenance.qualification import ( + CandidatePair, + GateResult, + QualificationHarness, + QualificationReceipt, +) +from policyengine.provenance.receipt import content_sha256 +from policyengine.provenance.wheels import ( + verify_installed_against_wheel, +) +from policyengine.utils.hashing import sha256_file + +Evidence = dict[str, Any] +GateOutcome = tuple[GateResult, Evidence] + +UK_SINGLE_YEAR_KEYS = ("time_period", "person", "benunit", "household") +ENTITY_TABLES = ("person", "benunit", "household") + + +def _json_safe(value: Any) -> Any: + """Evidence as strict JSON: NumPy scalars become the Python numbers they + hold (never their text), and non-finite numbers become their names.""" + + numpy = sys.modules.get("numpy") + if numpy is not None and isinstance( + value, (numpy.bool_, numpy.integer, numpy.floating) + ): + value = value.item() + if isinstance(value, float) and not math.isfinite(value): + return ( + "NaN" if math.isnan(value) else ("Infinity" if value > 0 else "-Infinity") + ) + if isinstance(value, Mapping): + return {str(key): _json_safe(item) for key, item in value.items()} + if isinstance(value, (list, tuple)): + return [_json_safe(item) for item in value] + return value + + +def evidence_sha256(evidence: Any) -> str: + """Digest of evidence that may contain non-finite numbers.""" + + return content_sha256(_json_safe(evidence)) + + +def _result( + gate_id: str, + status: str, + summary: str, + evidence: Evidence, + details: Optional[Mapping[str, Any]] = None, +) -> GateOutcome: + result = GateResult( + gate_id=gate_id, + status=status, # type: ignore[arg-type] + summary=summary, + evidence_sha256=evidence_sha256(evidence), + details=_json_safe(dict(details or {})), + ) + return result, _json_safe(evidence) + + +# --- 1. Installed versions --------------------------------------------------- + + +def installed_versions( + pair: CandidatePair, + harness: QualificationHarness, + *, + metadata: Any = importlib_metadata, +) -> GateOutcome: + """Installed wrapper, model and core equal the candidate; requirements hold.""" + + expected = { + "policyengine": harness.policyengine_version, + pair.model.name: pair.model.version, + pair.core.name: pair.core.version, + } + installed: dict[str, Optional[str]] = {} + problems: list[str] = [] + for name, version in expected.items(): + try: + installed[name] = metadata.version(name) + except metadata.PackageNotFoundError: + installed[name] = None + problems.append(f"{name} is not installed") + continue + if installed[name] != version: + problems.append(f"{name} {installed[name]} is installed, not {version}") + + unmet: list[str] = [] + for name in expected: + if installed.get(name) is None: + continue + for line in metadata.requires(name) or []: + requirement = Requirement(line) + if requirement.marker is not None and not requirement.marker.evaluate( + {"extra": ""} + ): + continue + try: + found = metadata.version(requirement.name) + except metadata.PackageNotFoundError: + unmet.append(f"{name} needs {requirement}, which is not installed") + continue + if requirement.specifier and not requirement.specifier.contains( + found, prereleases=True + ): + unmet.append(f"{name} needs {requirement}; {found} is installed") + problems.extend(unmet) + evidence = { + "expected": expected, + "installed": installed, + "unmet_requirements": unmet, + } + if problems: + return _result("installed_versions", "fail", "; ".join(problems), evidence) + return _result( + "installed_versions", + "pass", + f"policyengine {harness.policyengine_version}, {pair.model.name} {pair.model.version} and {pair.core.name} {pair.core.version} are installed and " + "their requirements hold.", + evidence, + ) + + +# --- 2. Wheel hashes ----------------------------------------------------------- + + +def wheel_hashes( + pair: CandidatePair, + *, + wheel_paths: Mapping[str, Path], + pypi_digests: Mapping[str, str], + metadata: Any = importlib_metadata, +) -> GateOutcome: + """Downloaded wheels match PyPI and the pin; installed files match the wheels.""" + + evidence: Evidence = {} + problems: list[str] = [] + for package in (pair.model, pair.core): + path = wheel_paths.get(package.name) + if path is None or not Path(path).is_file(): + problems.append(f"no downloaded wheel for {package.name}") + continue + actual = sha256_file(path) + if actual != package.wheel_sha256: + problems.append( + f"{package.name} wheel is {actual}, pinned {package.wheel_sha256}" + ) + if pypi_digests.get(package.name) != package.wheel_sha256: + problems.append(f"PyPI publishes a different {package.name} wheel digest") + verification = verify_installed_against_wheel( + package.name, Path(path), metadata=metadata + ) + evidence[package.name] = verification + problems.extend(f"{package.name} {item}" for item in verification["problems"]) + if problems: + return _result("wheel_hashes", "fail", "; ".join(problems[:10]), evidence) + counts = ", ".join( + f"{name}: {item['archive_members_verified']} wheel members, " + f"{item['files_verified']} installed files" + for name, item in evidence.items() + ) + return _result( + "wheel_hashes", + "pass", + f"The pinned PyPI wheels match their RECORDs and the installed files ({counts}).", + evidence, + ) + + +def receipt_wheel_paths( + pair: CandidatePair, wheel_paths: Mapping[str, Path] +) -> dict[str, Path]: + """``wheel_paths`` for ``build_execution_receipt``, which re-verifies them.""" + + return { + package.name: Path(wheel_paths[package.name]) + for package in (pair.model, pair.core) + if package.name in wheel_paths + } + + +# --- 3. Core compatibility ----------------------------------------------------- + + +def core_compatibility( + pair: CandidatePair, + release_manifest: DataReleaseManifest, + *, + model_requires_core: str, +) -> GateOutcome: + """The core satisfies the model; publisher claims are recorded, not required.""" + + problems: list[str] = [] + if not SpecifierSet(model_requires_core).contains( + pair.core.version, prereleases=True + ): + problems.append( + f"{pair.model.name} {pair.model.version} requires policyengine-core" + f"{model_requires_core}; the candidate core is {pair.core.version}" + ) + build = release_manifest.build + built_with = build.built_with_model_package if build is not None else None + if built_with is not None and built_with.name != pair.model.name: + problems.append( + f"the data was built with {built_with.name}, not {pair.model.name}" + ) + model_claims = [ + package.specifier + for package in release_manifest.compatible_model_packages + if package.name == pair.model.name + ] + core_claims = [ + package.specifier + for package in release_manifest.compatible_core_packages + if package.name == pair.core.name + ] + evidence = { + "model_requires_core": model_requires_core, + "built_with_model_version": built_with.version if built_with else None, + "built_with_core_version": ( + build.built_with_core_package.version + if build is not None and build.built_with_core_package is not None + else None + ), + "publisher_model_claims": model_claims, + "publisher_core_claims": core_claims, + "publisher_claim_covers_model": any( + _specifier_matches(pair.model.version, item) for item in model_claims + ), + "publisher_claim_covers_core": any( + _specifier_matches(pair.core.version, item) for item in core_claims + ), + } + if problems: + return _result("core_compatibility", "fail", "; ".join(problems), evidence) + coverage = ( + "the publisher's claims cover both" + if evidence["publisher_claim_covers_model"] + and evidence["publisher_claim_covers_core"] + else "certification will record an exact qualified pair, since the publisher's claims do not cover both" + ) + return _result( + "core_compatibility", + "pass", + f"policyengine-core {pair.core.version} satisfies {pair.model.name}{model_requires_core}; {coverage}.", + evidence, + details={ + "publisher_claim_covers_model": evidence["publisher_claim_covers_model"], + "publisher_claim_covers_core": evidence["publisher_claim_covers_core"], + }, + ) + + +# --- 4. Data access ------------------------------------------------------------ + + +def _hf_url(repo_id: str, repo_type: str, revision: str, path: str) -> str: + prefix = "datasets/" if repo_type == "dataset" else "" + return f"https://huggingface.co/{prefix}{repo_id}/resolve/{revision}/{path}" + + +def data_access( + pair: CandidatePair, + *, + get: Callable[..., Any], + head: Callable[..., Any], + token: Optional[str], +) -> tuple[GateResult, Evidence, Optional[DataReleaseManifest]]: + """Authenticated access to the exact manifest and every artifact.""" + + data = pair.data + evidence: Evidence = {"artifacts": {}} + if not token: + result, evidence = _result( + "data_access", + "fail", + "No Hugging Face token is configured; the UK data is private.", + evidence, + ) + return result, evidence, None + headers = {"Authorization": f"Bearer {token}"} + manifest_url = _hf_url( + data.repo_id, + data.repo_type, + data.release_manifest_revision, + data.release_manifest_path, + ) + response = get(manifest_url, headers=headers, timeout=60) + evidence["manifest_status"] = response.status_code + if response.status_code != 200: + result, evidence = _result( + "data_access", + "fail", + f"The release manifest returned HTTP {response.status_code}.", + evidence, + ) + return result, evidence, None + manifest_sha256 = hashlib.sha256(response.content).hexdigest() + evidence["manifest_sha256"] = manifest_sha256 + problems = [] + if manifest_sha256 != data.release_manifest_sha256: + problems.append("the release manifest bytes differ from the candidate's") + manifest = DataReleaseManifest.model_validate_json(response.content) + for name, artifact in sorted(manifest.artifacts.items()): + url = _hf_url( + artifact.repo_id, + artifact.repo_type or data.repo_type, + artifact.revision, + artifact.path, + ) + status = head( + url, headers=headers, timeout=60, allow_redirects=True + ).status_code + evidence["artifacts"][name] = status + if status != 200: + problems.append(f"{name} returned HTTP {status}") + dataset_url = _hf_url( + data.repo_id, data.repo_type, data.dataset_revision, data.dataset_path + ) + anonymous = head(dataset_url, timeout=60, allow_redirects=True).status_code + evidence["anonymous_dataset_status"] = anonymous + if anonymous == 200: + problems.append( + "the dataset is readable without credentials, so the authenticated path was not tested" + ) + if problems: + result, evidence = _result("data_access", "fail", "; ".join(problems), evidence) + return result, evidence, manifest + result, evidence = _result( + "data_access", + "pass", + f"Read the release manifest and {len(manifest.artifacts)} artifacts with the " + f"configured credential; anonymous access returned HTTP {anonymous}.", + evidence, + ) + return result, evidence, manifest + + +# --- 5. Dataset bytes ---------------------------------------------------------- + + +def h5_keys(path: Path) -> list[str]: + import h5py + + with h5py.File(path, "r") as stream: + return sorted(stream.keys()) + + +def dataset_bytes(pair: CandidatePair, dataset_path: Path) -> GateOutcome: + """The simulated file is the candidate's bytes and has the UK layout.""" + + data = pair.data + actual = sha256_file(dataset_path) + size = dataset_path.stat().st_size + keys = h5_keys(dataset_path) + evidence = {"sha256": actual, "size_bytes": size, "h5_keys": keys} + problems = [] + if actual != data.dataset_sha256: + problems.append(f"sha256 {actual}, expected {data.dataset_sha256}") + if size != data.size_bytes: + problems.append(f"{size} bytes, expected {data.size_bytes}") + missing_keys = [key for key in UK_SINGLE_YEAR_KEYS if key not in keys] + if missing_keys: + problems.append(f"missing H5 keys {missing_keys}") + if problems: + return _result("dataset_bytes", "fail", "; ".join(problems), evidence) + return _result( + "dataset_bytes", + "pass", + f"{data.dataset} is the pinned {size:,} bytes with the UK single-year layout.", + evidence, + ) + + +# --- 6. Supported years -------------------------------------------------------- + + +def data_year(dataset_path: Path) -> int: + import pandas as pd + + with pd.HDFStore(dataset_path, mode="r") as store: + return int(str(store["time_period"].iloc[0])[:4]) + + +def projected_years_from_model(dataset_path: Path) -> list[int]: + """The years policyengine-uk projects a single-year dataset to. + + Builds the multi-year dataset exactly as ``policyengine_uk.Simulation`` + does for a single-year file (``extend_single_year_dataset`` with the + system's parameters), without running any formula. + """ + + from policyengine_uk.data.dataset_schema import UKSingleYearDataset + from policyengine_uk.data.economic_assumptions import extend_single_year_dataset + from policyengine_uk.system import system + + multi_year = extend_single_year_dataset( + UKSingleYearDataset(str(dataset_path)), system.parameters + ) + years = getattr(multi_year, "years", None) + if years is None: + years = list(multi_year.datasets) + return sorted(int(year) for year in years) + + +def supported_years( + pair: CandidatePair, + dataset_path: Path, + *, + required_years: Sequence[int], + budget_horizon_years: Sequence[int], + previous_supported_years: Optional[Sequence[int]], + projected_years: Callable[[Path], list[int]] = projected_years_from_model, +) -> GateOutcome: + """Required years are inside the data's projection; support never shrinks. + + Support comes from the data and the model's projection, never from a + calculation succeeding: policyengine-core fills years outside the + projection (carrying or uprating the latest inputs), so a result there + is not evidence of support. + """ + + first = data_year(dataset_path) + years = projected_years(dataset_path) + supported = sorted(year for year in years if year >= first) + missing_required = sorted(set(required_years) - set(supported)) + lost = sorted(set(previous_supported_years or []) - set(supported)) + outside_horizon = sorted(set(budget_horizon_years) - set(supported)) + evidence = { + "data_year": first, + "supported_years": supported, + "required_years": sorted(required_years), + "budget_horizon_years": sorted(budget_horizon_years), + "previous_supported_years": sorted(previous_supported_years or []), + "lost_years": lost, + "outside_projection": outside_horizon, + } + details = {"supported_years": supported, "outside_projection": outside_horizon} + if not supported: + return _result( + "supported_years", + "fail", + f"The dataset supports no year: its data year is {first} and the model " + f"projects only to {max(years) if years else 'nothing'}.", + evidence, + details, + ) + if missing_required or lost: + parts = [] + if missing_required: + parts.append( + f"required years {missing_required} are outside the projection" + ) + if lost: + parts.append(f"years {lost} were supported by the previous certificate") + return _result("supported_years", "fail", "; ".join(parts), evidence, details) + if outside_horizon: + return _result( + "supported_years", + "needs_review", + f"Budget horizon years {outside_horizon} are outside the dataset's projection " + f"({supported[0]}–{supported[-1]}); results there are provisional.", + evidence, + details, + ) + return _result( + "supported_years", + "pass", + f"Supports {supported[0]}–{supported[-1]}, covering the required and horizon years.", + evidence, + details, + ) + + +# --- 7. Budget inputs ---------------------------------------------------------- + + +def dataset_columns(dataset_path: Path) -> dict[str, Any]: + """``{entity: DataFrame}`` for the UK entity tables.""" + + import pandas as pd + + with pd.HDFStore(dataset_path, mode="r") as store: + return { + entity: store[entity] for entity in ENTITY_TABLES if f"/{entity}" in store + } + + +def _degenerate(series: Any) -> bool: + values = series.dropna() + if len(values) == 0 or values.nunique() <= 1: + return True + if values.dtype.kind in "biuf": + return bool((values.astype(float) == 0).all()) + return False + + +def budget_inputs( + pair: CandidatePair, + dataset_path: Path, + *, + model_input_variables: Iterable[str], + critical_inputs: Sequence[str], + accepted_gaps: Mapping[str, str], + previous_missing_inputs: Optional[Sequence[str]], + previous_unusable_critical: Optional[Sequence[str]], + tables: Optional[Mapping[str, Any]] = None, +) -> GateOutcome: + """Critical inputs are present and usable; no input goes newly missing.""" + + tables = dict(tables) if tables is not None else dataset_columns(dataset_path) + columns: dict[str, str] = {} + for entity, table in tables.items(): + for column in table.columns: + columns.setdefault(column, entity) + inputs = set(model_input_variables) + missing = sorted(inputs - set(columns)) + newly_missing = ( + sorted(set(missing) - set(previous_missing_inputs)) + if previous_missing_inputs is not None + else [] + ) + unusable = [] + for name in critical_inputs: + if name not in columns: + unusable.append((name, "missing")) + elif _degenerate(tables[columns[name]][name]): + unusable.append((name, "degenerate")) + previous_bad = set(previous_unusable_critical or []) + regressions = [(n, why) for n, why in unusable if n not in previous_bad] + standing = [(n, why) for n, why in unusable if n in previous_bad] + unaccepted_standing = [(n, why) for n, why in standing if n not in accepted_gaps] + evidence = { + "model_input_variables": len(inputs), + "dataset_columns": len(columns), + "missing_inputs": missing, + "newly_missing_inputs": newly_missing, + "unusable_critical": [list(item) for item in unusable], + "accepted_gaps": dict(accepted_gaps), + } + details = { + "missing_inputs": len(missing), + "newly_missing_inputs": newly_missing, + "unusable_critical": [name for name, _ in unusable], + } + if regressions: + return _result( + "budget_inputs", + "fail", + "Budget-critical inputs became unusable: " + + ", ".join(f"{name} ({why})" for name, why in regressions), + evidence, + details, + ) + if unaccepted_standing or newly_missing: + parts = [] + if newly_missing: + parts.append( + f"{len(newly_missing)} model inputs are newly missing from the data " + f"({', '.join(newly_missing[:8])}{'…' if len(newly_missing) > 8 else ''})" + ) + if unaccepted_standing: + parts.append( + "critical inputs unusable in both pairs: " + + ", ".join(f"{name} ({why})" for name, why in unaccepted_standing) + ) + return _result( + "budget_inputs", "needs_review", "; ".join(parts), evidence, details + ) + return _result( + "budget_inputs", + "pass", + f"All {len(critical_inputs)} critical inputs are present and usable; " + f"no model input is newly missing ({len(missing)} optional inputs absent, as before).", + evidence, + details, + ) + + +# --- 8. Reform contract -------------------------------------------------------- + + +def _load_entrypoint( + entrypoint: str, importer: Callable[[str], Any] +) -> tuple[Callable[..., Any], Any]: + """The entrypoint's callable and the module that defines it.""" + + module_name, _, attribute = entrypoint.partition(":") + if not attribute: + module_name, _, attribute = entrypoint.rpartition(".") + module = importer(module_name) + function: Callable[..., Any] = getattr(module, attribute) + return function, module + + +def _contract_coverage(module: Any) -> Optional[dict[str, Any]]: + """What a full contract run covers, as the runner's own module declares it. + + ``ROUTES`` (route ids), ``CASES`` (objects with an ``id`` and optional + ``routes`` limiting where the case runs and ``checks`` describing each + expected value), ``UNSUPPORTED_ROUTES`` and ``PROPERTIES`` (keyed by + property id). ``None`` when the module does not declare the essentials. + """ + + try: + routes = [str(route) for route in module.ROUTES] + pairs = [ + (str(case.id), str(route)) + for case in module.CASES + for route in ( + case.routes if getattr(case, "routes", None) is not None else routes + ) + ] + checks: list[tuple[str, str, Optional[str]]] = [] + assertions: dict[tuple[str, str, str, str], dict[str, Any]] = {} + unsupported = getattr(module, "UNSUPPORTED_ROUTES", {}) + country_routes = set( + getattr( + module, + "COUNTRY_ROUTES", + [route for route in routes if route.startswith("country.")], + ) + ) + unsupported_checks = {} + for case in module.CASES: + for route in ( + case.routes if getattr(case, "routes", None) is not None else routes + ): + if route in unsupported: + key = (str(case.id), str(route), None) + checks.append(key) + unsupported_checks[key] = unsupported[route] + elif getattr(case, "expect_error", None) is not None: + checks.append((str(case.id), str(route), "rejected")) + else: + for index, check in enumerate(getattr(case, "checks", ())): + key = ( + str(case.id), + str(route), + f"{check.target}:{check.name}:{check.year}", + ) + checks.append(key) + assertion_id = f"{key[2]}:{index}" + assertions[(*key, assertion_id)] = { + "oracle": getattr(check, "oracle", None), + "pending": getattr(check, "pending", None), + } + # evaluate_case declares these per-check exemptions; + # route-wide exemptions above replace the entire case. + if check.target == "parameter" and route not in country_routes: + unsupported_checks[key] = ( + "this route exposes outputs, not parameter values" + ) + elif ( + check.target == "baseline_equals" + and route == "wrapper.policy" + ): + unsupported_checks[key] = ( + "the policy route's output dataset holds the reformed run only" + ) + + # The runner has no machine-readable isolation matrix. + # Derive its strongest coverage from _CountryRoute.run, + # _WrapperHouseholdRoute.run and _WrapperPolicyRoute.run: + # deduplicate requests; country baselines cover variable + # requests and after-runs cover parameters and variables; + # wrappers' after-runs cover exposed variables only. + # baseline_equals also requests its expected variable on + # country/household routes, but policy excludes that target. + # Rejected/error and whole-route unsupported cases emit no + # isolation rows. Every supported run also emits the shared + # module parameter check, required independently below. + requests = [] + for check in getattr(case, "checks", ()): + if route not in country_routes and ( + check.target == "parameter" + or ( + route == "wrapper.policy" + and check.target == "baseline_equals" + ) + ): + continue + kind = ( + "parameter" if check.target == "parameter" else "variable" + ) + requests.append((kind, str(check.name), check.year)) + if check.target == "baseline_equals": + requests.append( + ("variable", str(check.expected), check.year) + ) + for kind, name, year in dict.fromkeys(requests): + if route in country_routes and kind == "variable": + checks.append( + ( + str(case.id), + str(route), + f"baseline_isolation:{name}:{year}", + ) + ) + checks.append( + ( + str(case.id), + str(route), + f"unreformed_after_reform:{name}:{year}", + ) + ) + return { + "routes": routes, + "pairs": pairs, + "properties": [str(name) for name in module.PROPERTIES], + "checks": checks, + "isolation_checks": { + (case, route, check) + for case, route, check in checks + if isinstance(check, str) + and check.startswith( + ("baseline_isolation:", "unreformed_after_reform:") + ) + }, + "assertions": assertions, + "pending_decisions": { + str(decision) for decision in getattr(module, "PENDING_DECISIONS", ()) + }, + "unsupported_routes": unsupported, + "unsupported_checks": unsupported_checks, + "run_pairs": ( + [("module_parameters_unchanged", "all")] + if any(route not in unsupported for route in routes) + else [] + ), + "population": getattr(module, "POPULATION_CHECK_ROUTES", None), + } + except AttributeError: + return None + + +def _declared_population_checks( + module_name: str, importer: Callable[[str], Any], routes: Sequence[str] +) -> list[tuple[str, str]]: + """The runner's complete population check/route multiset.""" + + module = importer(module_name.rsplit(".", 1)[0] + ".budget_reform_population") + declared = getattr(module, "POPULATION_CHECK_ROUTES", None) + if declared is not None: + return [(str(check), str(route)) for check, route in declared] + coverage = getattr(module, "population_check_routes", None) + if callable(coverage): + return [(str(check), str(route)) for check, route in coverage(routes)] + # IDs alone cannot prove that every route ran. Retain their names in the + # diagnostic, while refusing a runner with no per-route declaration. + ids = getattr(module, "POPULATION_CHECKS", None) or re.findall( + r"^``([a-z][a-z0-9_]+)``$", inspect.getdoc(module) or "", re.MULTILINE + ) + raise AttributeError( + "the population runner declares no population check routes" + + (": " + ", ".join(str(check) for check in ids) if ids else "") + ) + + +def _reported_assertions( + rows: Sequence[Mapping[str, Any]], coverage: Mapping[str, Any] +) -> tuple[dict[int, tuple[str, str, str, str]], list[str]]: + """Resolve current assertion IDs and unambiguous historical oracle rows. + + Released reports predate assertion IDs. Their oracles distinguish checks + with the same label. Whole-route exemptions have no values or oracles; + their interchangeable rows are consumed in declaration order instead. + """ + resolved = {} + problems = [] + used: Counter[tuple[str, str, str, str]] = Counter() + by_label: dict[tuple[str, str, str], list[tuple[str, str, str, str]]] = {} + for key in coverage["assertions"]: + by_label.setdefault(key[:3], []).append(key) + for row in rows: + label = (str(row.get("case_id")), str(row.get("route")), row.get("check")) + matches = list(by_label.get(label, ())) + if not matches: + continue # Isolation, run-level and rejected-case rows are separate. + assertion_id = row.get("assertion_id") + if assertion_id is not None: + matches = [key for key in matches if key[3] == assertion_id] + if row.get("oracle") is not None: + matches = [ + key + for key in matches + if coverage["assertions"][key]["oracle"] == row["oracle"] + ] + elif len(matches) > 1: + if row.get("oracle") is not None: + matches = [ + key + for key in matches + if coverage["assertions"][key]["oracle"] == row["oracle"] + ] + elif ( + row.get("status") == "unsupported" + and label in coverage["unsupported_checks"] + ): + matches = [key for key in matches if not used[key]][:1] + else: + matches = [] + if len(matches) != 1: + problems.append( + f"case {label[0]}@{label[1]}:{label[2]} has no unique declared assertion identity" + ) + continue + resolved[id(row)] = matches[0] + used[matches[0]] += 1 + return resolved, problems + + +#: Report sections: the key that names each row, and what a row is called. +_CONTRACT_SECTIONS = ( + ("cases", "case_id", "case"), + ("properties", "property_id", "property"), + ("population", "check_id", "population check"), +) +_CONTRACT_STATUSES = ("pass", "fail", "error", "unsupported", "pending") +#: How the runner's run-level row names the shared parameter check. Contract +#: 1.0.0 continued "in ..."; 1.1.0 added node metadata and band order before +#: it, so only this stem is fixed. +RUN_LEVEL_CHECK_PREFIX = "every parameter's dated values and metadata" + + +def reform_contract( + *, + entrypoint: Optional[str], + dataset_path: Optional[Path], + seed: int, + max_examples: int, + expected_versions: Optional[Mapping[str, str]] = None, + expected_dataset_sha256: Optional[str] = None, + pending_decisions: Sequence[str] = (), + importer: Callable[[str], Any] = importlib.import_module, +) -> GateOutcome: + """Run the Budget reform contract (W1) on every route and gate on its report. + + ``fail`` when any case, sampled property or population check fails or + errors. ``error`` when the report cannot be trusted as a full run of this + candidate: + - it does not cover every declared case check on each route, the runner's + isolation checks and run-level shared-parameter check, every declared + population check and every sampled property, or a property passed on + fewer examples than requested; + - a property is pending, a pending row names no authorized decision, + or an unsupported case or population check has no runner-declared + exemption with the same ``reason``; + - a sampled property did not run; + - population checks are missing although a dataset was given; + - the report names other versions, another dataset, seed or sample size + than requested; + - its own ``holds`` or ``summary`` disagrees with its rows. + """ + + if not entrypoint: + return _result( + "reform_contract", + "unavailable", + "No reform contract runner is configured.", + {}, + ) + try: + runner, runner_module = _load_entrypoint(entrypoint, importer) + except (ImportError, AttributeError) as exc: + return _result( + "reform_contract", + "unavailable", + f"The reform contract runner {entrypoint} is not importable: {exc}", + {"entrypoint": entrypoint}, + ) + try: + report = runner( + routes=None, + dataset_path=str(dataset_path) if dataset_path else None, + seed=seed, + max_examples=max_examples, + ) + except Exception as exc: # the candidate made the contract crash + return _result( + "reform_contract", + "error", + f"The reform contract raised {type(exc).__name__}: {exc}", + {"entrypoint": entrypoint, "traceback": traceback.format_exc()}, + ) + report = json.loads(json.dumps(_json_safe(report), default=str)) + failures: list[str] = [] + malformed: list[str] = [] + pending: list[str] = [] + unsupported: list[str] = [] + counts = dict.fromkeys(_CONTRACT_STATUSES, 0) + coverage = _contract_coverage(runner_module) + reported_assertions = {} + if coverage is not None: + reported_assertions, assertion_problems = _reported_assertions( + report.get("cases") or [], coverage + ) + malformed.extend(assertion_problems) + population_exemptions = {} + population_module = None + if dataset_path is not None: + module_name = ( + entrypoint.partition(":")[0] + if ":" in entrypoint + else entrypoint.rpartition(".")[0] + ) + try: + population_module = importer( + module_name.rsplit(".", 1)[0] + ".budget_reform_population" + ) + population_exemptions = getattr(population_module, "UNSUPPORTED_CHECKS", {}) + except (ImportError, AttributeError): + pass + allowed_pending = set(pending_decisions) | ( + coverage["pending_decisions"] if coverage else set() + ) + for section, key, noun in _CONTRACT_SECTIONS: + for row in report.get(section) or []: + status = row.get("status") + label = f"{noun} {row.get(key)}" + ( + f"@{row['route']}" if row.get("route") else "" + ) + if status in counts: + counts[status] += 1 + if status in ("fail", "error"): + failures.append(label) + elif status == "pending": + decision = row.get("decision_id") + declared_decision = ( + coverage["assertions"] + .get(reported_assertions.get(id(row)), {}) + .get("pending") + if coverage and section == "cases" + else None + ) + if section == "properties": + malformed.append( + f"{label} did not run: properties cannot be pending" + ) + elif section == "population": + malformed.append( + f"{label} is a mandatory assertion: population checks cannot be pending" + ) + elif not decision: + malformed.append(f"{label} is pending without a decision_id") + elif section == "cases" and declared_decision is None: + malformed.append( + f"{label} is a mandatory assertion relabelled pending" + ) + elif section == "cases" and decision != declared_decision: + malformed.append( + f"{label} is pending for undeclared decision {decision}" + ) + elif decision != declared_decision and decision not in allowed_pending: + malformed.append( + f"{label} is pending for undeclared decision {decision}" + ) + pending.append(label) + elif status == "unsupported": + if section == "properties": + malformed.append( + f"{label} did not run: {row.get('reason') or 'no reason given'}" + ) + elif not row.get("reason"): + malformed.append(f"{label} is unsupported without a reason") + elif section == "cases": + declared_reason = ( + coverage["unsupported_checks"].get( + ( + str(row.get("case_id")), + str(row.get("route")), + row.get("check"), + ) + ) + if coverage + else None + ) + if declared_reason is None: + malformed.append(f"{label} is an undeclared unsupported check") + elif row["reason"] != declared_reason: + malformed.append( + f"{label} has a different reason than its declared exemption" + ) + elif section == "population": + # The population runner currently declares no per-check + # exemptions. Its no-country-route sentinel cannot arise + # in a full run; only explicit runner declarations exempt + # a check, never a reason supplied by the report itself. + declared_reason = population_exemptions.get( + (str(row.get("check_id")), str(row.get("route"))) + ) + if declared_reason is None: + malformed.append(f"{label} is an undeclared unsupported check") + elif row["reason"] != declared_reason: + malformed.append( + f"{label} has a different reason than its declared exemption" + ) + unsupported.append(label) + elif status != "pass": + malformed.append(f"{label} has unknown status {status!r}") + if not report.get("cases"): + malformed.append("the report has no cases") + # A full run covers what the runner itself declares: every route, every + # case on each route it runs on, and every sampled property with the + # examples asked for. A report that drops any of it does not qualify. + if coverage is None: + # Lead with the missing runner declarations rather than truncating + # that root cause behind individual rows whose exemptions are unknown. + malformed.insert( + 0, + "the runner declares no ROUTES, CASES and PROPERTIES, so the " + "report's coverage cannot be checked", + ) + else: + reported_routes = [str(route) for route in report.get("routes") or []] + if sorted(reported_routes) != sorted(coverage["routes"]): + malformed.append( + f"the report covers routes {sorted(reported_routes)}, not " + f"{sorted(coverage['routes'])}" + ) + covered = { + (str(row.get("case_id")), str(row.get("route"))) + for row in report.get("cases") or [] + } + absent = [ + f"{case}@{route}" + for case, route in coverage["pairs"] + if (case, route) not in covered + ] + if absent: + malformed.append( + f"{len(absent)} case-route pair(s) have no row: " + + ", ".join(absent[:4]) + ) + reported_checks = Counter( + (str(row.get("case_id")), str(row.get("route")), row.get("check")) + for row in report.get("cases") or [] + ) + # W1 replaces a case-route's checks with a single error row when + # constructing its simulation crashes. That is a complete attempted + # run and already fails the gate; it cannot exempt a passing report. + crashed = { + (str(row.get("case_id")), str(row.get("route"))) + for row in report.get("cases") or [] + if row.get("status") == "error" + and row.get("check") is None + and row.get("detail") + } + absent_checks = ( + Counter(key for key in coverage["checks"] if key[:2] not in crashed) + - reported_checks + ) + if absent_checks: + malformed.append( + f"{sum(absent_checks.values())} declared case check(s) have no row: " + + ", ".join( + f"{case}@{route}:{check}" + for case, route, check in list(absent_checks)[:4] + ) + ) + # A pending decision belongs to one declared assertion. Count each + # assertion separately and never let pending rows satisfy mandatory + # rows, even where the runner reads the same parameter twice. + for category in ("mandatory", "pending"): + expected = Counter( + key + for key, declaration in coverage["assertions"].items() + if key[:2] not in crashed + and ( + "pending" + if declaration["pending"] is not None + and key[:3] not in coverage["unsupported_checks"] + else "mandatory" + ) + == category + ) + actual = Counter( + reported_assertions[id(row)] + for row in report.get("cases") or [] + if id(row) in reported_assertions + and ("pending" if row.get("status") == "pending" else "mandatory") + == category + ) + absent_assertions = expected - actual + excess_assertions = actual - expected + if absent_assertions or excess_assertions: + malformed.append( + f"{category} assertion multiplicities differ: " + f"{sum(absent_assertions.values())} missing, " + f"{sum(excess_assertions.values())} extra; " + + ", ".join( + f"{case}@{route}:{assertion_id}" + for case, route, _, assertion_id in list( + absent_assertions or excess_assertions + )[:4] + ) + ) + for case, route in coverage["run_pairs"]: + rows = [ + row + for row in report.get("cases") or [] + if row.get("case_id") == case and row.get("route") == route + ] + if len(rows) != 1: + malformed.append( + f"run-level check {case}@{route} must have exactly one row" + ) + elif rows[0].get("status") not in ("pass", "fail", "error"): + malformed.append(f"run-level check {case}@{route} did not run") + elif not (rows[0].get("check") or "").startswith(RUN_LEVEL_CHECK_PREFIX): + malformed.append( + f"run-level check {case}@{route} does not identify the shared parameter check" + ) + for row in report.get("cases") or []: + key = (str(row.get("case_id")), str(row.get("route")), row.get("check")) + if key in coverage["isolation_checks"] and row.get("status") not in ( + "pass", + "fail", + "error", + ): + # The runner emits pass/fail for isolation, never a methodology + # exemption. An explicit pending-decision override must not + # excuse a missing executed isolation check. + malformed.append( + f"case {key[0]}@{key[1]} isolation check {key[2]} did not run" + ) + if row.get("route") in coverage["unsupported_routes"] and ( + row.get("check") is not None or row.get("status") != "unsupported" + ): + malformed.append( + f"case {row.get('case_id')}@{row.get('route')} is not the declared unsupported route row" + ) + if dataset_path is not None: + declared_population = coverage["population"] + if declared_population is None: + try: + module_name = ( + entrypoint.partition(":")[0] + if ":" in entrypoint + else entrypoint.rpartition(".")[0] + ) + declared_population = _declared_population_checks( + module_name, importer, coverage["routes"] + ) + except (ImportError, AttributeError) as exc: + malformed.append(str(exc)) + declared_population = [] + if not declared_population: + malformed.append( + "the runner declares no population checks, so population coverage cannot be checked" + ) + else: + population = Counter( + (str(row.get("check_id")), str(row.get("route"))) + for row in report.get("population") or [] + ) + expected_population = Counter(declared_population) + absent_population = expected_population - population + excess_population = population - expected_population + if absent_population or excess_population: + malformed.append( + "declared population check route multiplicities differ: " + f"{sum(absent_population.values())} missing, " + f"{sum(excess_population.values())} extra; " + + ", ".join( + f"{check}@{route}" + for check, route in list( + absent_population or excess_population + ) + ) + ) + properties = { + str(row.get("property_id")): row for row in report.get("properties") or [] + } + for name in coverage["properties"]: + row = properties.get(name) + if row is None: + malformed.append(f"property {name} has no row") + elif row.get("status") == "pass": + examples = row.get("examples_run") + if ( + isinstance(examples, bool) + or not isinstance(examples, int) + or examples < max_examples + or examples < 1 + ): + malformed.append( + f"property {name} passed on {examples} example(s), " + f"not the {max_examples} requested" + ) + if dataset_path is not None and not report.get("population"): + malformed.append("a dataset was given but no population checks ran") + for field, requested in (("seed", seed), ("max_examples", max_examples)): + if field in report and report[field] != requested: + malformed.append(f"the report ran {field}={report[field]}, not {requested}") + reported_versions = report.get("versions") or {} + for package, version in (expected_versions or {}).items(): + if reported_versions.get(package) != version: + malformed.append( + f"the report ran {package} {reported_versions.get(package)}, " + f"not the candidate's {version}" + ) + if expected_dataset_sha256 is not None: + reported = (report.get("dataset") or {}).get("sha256") + if reported != expected_dataset_sha256: + malformed.append( + f"the report ran dataset {reported}, not the candidate's " + f"{expected_dataset_sha256}" + ) + if "holds" in report and report["holds"] != ( + counts["fail"] == 0 and counts["error"] == 0 + ): + malformed.append("the report's holds disagrees with its rows") + summary = report.get("summary") + if ( + isinstance(summary, Mapping) + and {status: summary.get(status, 0) for status in _CONTRACT_STATUSES} != counts + ): + malformed.append("the report's summary disagrees with its rows") + published = publishable_contract_report(report) + details = { + "contract_id": report.get("contract_id"), + "contract_version": report.get("contract_version"), + # The digest of what is published: one of the full report would let + # anyone recover a suppressed count by trying the nine values. + "report_sha256": evidence_sha256(published), + "counts": counts, + "pending": pending, + "unsupported": unsupported, + "problems": malformed, + } + if failures: + return _result( + "reform_contract", + "fail", + f"{len(failures)} contract row(s) failed: {', '.join(failures[:8])}", + published, + details, + ) + if malformed: + return _result( + "reform_contract", "error", "; ".join(malformed[:8]), published, details + ) + return _result( + "reform_contract", + "pass", + f"Contract {details['contract_version']}: every case, property and " + f"population check passed ({counts['pass']})" + + (f"; {len(pending)} pending a methodology call" if pending else "") + + (f"; {len(unsupported)} unsupported by design" if unsupported else "") + + ".", + published, + details, + ) + + +#: Written in place of a value from the survey data that the ledger does +#: not publish. +WITHHELD_VALUE = "withheld" + + +#: The population row fields W1's runner writes that carry no survey value +#: (``budget_reform_population._row``, its identity fields and the run's +#: fixed subsample size); they are published as reported. ``detail`` goes +#: through ``_publishable_detail``; any other field is withheld. +POPULATION_ROW_FIELDS = frozenset( + { + "check_id", + "route", + "status", + "reason", + "seed", + "subsample_households", + "reform_identity", + "reform_identity_unavailable", + } +) + + +def _finding(value: Any) -> Any: + return value if isinstance(value, bool) or value is None else WITHHELD_VALUE + + +def _publishable_detail(detail: Any) -> Any: + """A population row's ``detail`` as the ledger may publish it: its + top-level true/false findings, with every other value withheld. + + Population rows are computed on the survey data, and their numbers + combine: rows of one check on different routes share a subsample, so + two routes' totals or counts differ by what a handful of records did + (supp-r4 on #574), and two candidates' differ in the same way (cos + d1601). No per-value rule can bound every combination, and no gate rule + reads ``detail``. So only ``True``, ``False`` and ``None`` at the top of + a mapping are published, under the runner's own keys: not counts, + totals, amounts or text, and no nested list or mapping, whose length or + keys could be a count or name records (supp-r5). + """ + + if not isinstance(detail, Mapping): + return _finding(detail) + return {key: _finding(value) for key, value in detail.items()} + + +def publishable_contract_report(report: Mapping[str, Any]) -> dict[str, Any]: + """The reform contract report as the qualification ledger publishes it. + + The ledger is a branch of a public repository. Case and property rows + come from synthetic households and are published as reported. Population + rows are computed on the survey data: the fields in + ``POPULATION_ROW_FIELDS`` (check, route and status, which the gate + judges, among them) are published as reported, ``detail`` keeps only its + top-level true/false findings (``_publishable_detail``), and any other + field is withheld. The gate judges the report as the runner returned it. + To see a failed population check's figures, rerun the contract with + access to the dataset (``docs/engineering/runbooks/uk-qualification.md``). + """ + + published = dict(report) + if isinstance(report.get("population"), list): + published["population"] = [ + { + key: value + if key in POPULATION_ROW_FIELDS + else (_publishable_detail(value) if key == "detail" else WITHHELD_VALUE) + for key, value in row.items() + } + if isinstance(row, Mapping) + else row + for row in report["population"] + ] + return published + + +# --- 9. Metrics review --------------------------------------------------------- + + +def _metric_key(row: Mapping[str, Any]) -> tuple[str, Optional[int]]: + return (str(row["metric_id"]), row.get("year")) + + +def _bound(bounds: Mapping[str, Any], kind: Optional[str]) -> tuple[float, float]: + spec = (bounds.get("kinds") or {}).get(kind) or bounds.get("default") or {} + return float(spec.get("relative", 0.0)), float(spec.get("absolute", 0.0)) + + +#: Metric-report check prefixes (``identities[].identity_id``) that fail a +#: candidate whatever the previous pair did: none fail on main. +HARD_CHECK_PREFIXES = ( + "finite:", + "weights:", + "closure", + "identity:", + "determinism:", + "reform_runs:", + "responsive:", +) +#: Checks that already fail on main for known model defects (for example +#: ``counted_once:statutory_sick_pay``): they fail a candidate only when the +#: previous certified pair passed them. +REGRESSION_CHECK_PREFIXES = ( + "counted_once:", + "double_count_present:", + "overlap_guard:", + "sign:", + "bounds:", +) + + +#: The statuses the metrics tool gives a check. Anything else fails. +CHECK_STATUSES = frozenset({"pass", "warn", "fail", "skipped"}) + + +def classify_check(identity_id: str) -> str: + """``hard``, ``regression`` or ``hard`` for an unknown check (no skips).""" + + if identity_id.startswith(REGRESSION_CHECK_PREFIXES): + return "regression" + return "hard" + + +def _suppressed(row: Mapping[str, Any]) -> bool: + """Whether the tool published ``row`` as suppressed (fewer than 10 records).""" + + return row.get("suppressed") is True + + +def _excludes(row: Mapping[str, Any]) -> tuple[str, ...]: + """The suppressed figures a total is rebuilt without, in ``row``'s year.""" + + return tuple(sorted(str(metric_id) for metric_id in row.get("excludes") or ())) + + +#: The fields the pinned tool gives every metric row under its small-cell +#: rule (``api.to_report`` at policyengine-uk 1191ac5a4). A report without +#: them comes from a tool that predates suppression and may publish a figure +#: from fewer than 10 records, so it fails. +SMALL_CELL_FIELDS = ("suppressed", "excludes", "exclude_signs") + + +def _finite_number(value: Any) -> bool: + return ( + not isinstance(value, bool) + and isinstance(value, (int, float)) + and math.isfinite(value) + ) + + +def _valid_signs(row: Mapping[str, Any]) -> Optional[dict[str, int]]: + """``exclude_signs`` when it gives +1 or -1 for exactly the excluded ids.""" + + signs, excludes = row.get("exclude_signs"), row.get("excludes") + if not isinstance(signs, Mapping) or not isinstance(excludes, list): + return None + if set(signs) != set(excludes) or not all( + not isinstance(sign, bool) and sign in (1, -1) for sign in signs.values() + ): + return None + return {str(metric_id): int(sign) for metric_id, sign in signs.items()} + + +def _small_cell_problems(name: str, rows: Sequence[Mapping[str, Any]]) -> list[str]: + """Rows that break the tool's small-cell format, as problems. + + The tool publishes a figure from fewer than 10 records with ``value`` + null and ``suppressed`` true. A suppressed spending head's caseload is + suppressed with it; a published head's caseload counts only positive + records, so it can be suppressed on its own. Every total holding a + suppressed figure is rebuilt without it; the total lists the figure's + ``metric_id`` under ``excludes`` and the sign it enters with, +1 or -1, + under ``exclude_signs`` (``privacy.py``, ``measure.measure_year`` and + ``measure.rebuild_protected`` in the pinned tool). These are reports this + gate cannot read safely, so neither the gate's evidence nor the ledger + records any value from them: + + - a row without the small-cell fields, or whose year is not an integer; + - two rows for one figure and year (one could publish what the other + suppresses); + - a suppressed row with a value, or a ``suppressed`` that is not a + boolean; + - an ``excludes`` that is not a list of ids, or names a figure the same + report does not suppress in that year; + - ``exclude_signs`` that do not give +1 or -1 for exactly ``excludes``; + - a published caseload beside its suppressed spending head. + """ + + by_key = {(str(row.get("metric_id")), row.get("year")): row for row in rows} + hidden = {key for key, row in by_key.items() if _suppressed(row)} + repeated = sorted( + f"{key[0]} {key[1]}" + for key, count in Counter( + (str(row.get("metric_id")), row.get("year")) for row in rows + ).items() + if count > 1 + ) + missing_fields, bad_years, with_value, not_boolean = [], [], [], [] + malformed, bad_signs, unmatched, unpaired = [], [], [], [] + for row in rows: + metric_id, year = str(row.get("metric_id")), row.get("year") + label = f"{metric_id} {year}" + if any(field not in row for field in SMALL_CELL_FIELDS): + missing_fields.append(label) + if isinstance(year, bool) or not isinstance(year, int): + bad_years.append(label) + flag = row.get("suppressed", False) + if not isinstance(flag, bool): + not_boolean.append(label) + elif flag and row.get("value") is not None: + with_value.append(label) + if metric_id.startswith("caseloads.") and not _suppressed(row): + head = by_key.get((f"spending.{metric_id.split('.', 1)[1]}", year)) + if head is not None and _suppressed(head): + unpaired.append(label) + excludes = row.get("excludes", []) + if not isinstance(excludes, list) or not all( + isinstance(excluded, str) for excluded in excludes + ): + malformed.append(label) + continue + if "exclude_signs" in row and _valid_signs(row) is None: + bad_signs.append(label) + unmatched.extend( + f"{label} excludes {excluded}" + for excluded in excludes + if (excluded, year) not in hidden + ) + problems = [] + for found, what in ( + ( + missing_fields, + "rows without the small-cell fields (suppressed, excludes, " + "exclude_signs), so from a tool that predates suppression", + ), + (bad_years, "metric rows whose year is not an integer"), + (repeated, "duplicate metrics"), + (with_value, "suppressed figures with a value"), + (not_boolean, "a suppressed flag that is not true or false"), + (malformed, "excludes that are not a list of metric ids"), + (bad_signs, "exclude_signs that are not +1 or -1 for exactly its excludes"), + (unmatched, "totals excluding a figure it does not suppress"), + (unpaired, "caseloads published beside their suppressed spending head"), + ): + if found: + problems.append(f"the {name} report has {what}: {', '.join(found[:8])}") + return problems + + +#: Totals that are plain sums of heads in pounds, so a figure can be taken out +#: by subtraction: the pinned tool's ``compare._additive`` (sections +#: model_totals and balance, whose rows have kind ``model_total`` and +#: ``balance``, and household net income). Decile means and the HBAI total +#: (reset at zero) are not. +ADDITIVE_KINDS = frozenset({"model_total", "balance"}) +ADDITIVE_METRICS = frozenset({"household.household_net_income"}) + + +def _additive(row: Mapping[str, Any]) -> bool: + return ( + row.get("kind") in ADDITIVE_KINDS + or str(row.get("metric_id")) in ADDITIVE_METRICS + ) + + +def _aligned( + value: float, + own: Mapping[str, Any], + other_signs: Mapping[str, int], + metrics: Mapping[tuple[str, Optional[int]], Mapping[str, Any]], + year: Optional[int], +) -> Optional[float]: + """``own``'s total with every figure only the other side excludes taken out. + + As ``compare._aligned`` in the pinned tool: the figure's value is this + side's own published row for the year, and its sign is the one the side + that excludes it records. None when a needed figure is not published + here (missing, suppressed or not finite). + """ + + own_excludes = set(_excludes(own)) + for metric_id, sign in sorted(other_signs.items()): + if metric_id in own_excludes: + continue + published = metrics.get((metric_id, year)) + if ( + published is None + or _suppressed(published) + or not _finite_number(published.get("value")) + ): + return None + value -= sign * float(published["value"]) + return value + + +def _wider_failure(candidate: Mapping[str, Any], previous: Mapping[str, Any]) -> bool: + """Whether a check failing in both reports fails in a scope it did not before. + + Scopes are years, or ``""`` for a check with no year. Without + ``failing_scopes`` on both rows the two failures cannot be told apart, so + the failure counts as carried over. + """ + + now, before = candidate.get("failing_scopes"), previous.get("failing_scopes") + if not isinstance(now, list) or not isinstance(before, list): + return False + return not {str(scope) for scope in now} <= {str(scope) for scope in before} + + +def compare_metric_reports( + candidate: Mapping[str, Any], + previous: Mapping[str, Any], + bounds: Mapping[str, Any], +) -> tuple[str, str, Evidence]: + """``(status, summary, evidence)`` for a candidate against a previous report. + + Checks (``identities``) follow the metric tool's classification: hard + checks fail outright; known-defect checks fail only when they worsen + against the previous pair, meaning the previous pair did not fail them, + or failed them in fewer scopes (``failing_scopes``, when the tool reports + them); ``warn`` is recorded, and a check that newly warns (it did not + warn in the previous report, as the tool's ``new_warning``) needs + review. ``skipped`` means the check was not run, which is neither a pass + nor a failure: a skipped hard check, or a check skipped where the + previous pair ran it (pass, warn or fail), needs review. + + A report with a status other than ``ok``, no metrics or no checks fails, + as does a report that repeats a check, or any check, in either report, + with a status outside ``CHECK_STATUSES``. A hard check the previous pair + ran that is missing from the candidate fails (the two reports share one + tool and one set of definitions); other missing checks need review. + + Small cells (W3's tool publishes a figure from fewer than 10 records as + ``value`` null, ``suppressed`` true, and rebuilds every total that holds + one without it, listing it under ``excludes`` with its sign under + ``exclude_signs``): + + - a suppressed figure is present but never compared, and is not a + non-finite value; suppressed on both sides, there is nothing to compare; + - a figure suppressed on one side only is withheld: no change is + computed (it would come from fewer than 10 records) and the comparison + needs review; + - a total compares directly when both sides exclude the same figures + with the same signs; + - an additive total (``_additive``) whose sides exclude different + figures is lined up as the tool's ``compare._aligned`` does: each side + takes out, at its own published value and the excluding side's sign, + every figure only the other side excludes. The change between the + lined-up totals is held to the bound, whose relative part is taken on + the previous report's published total. Any other total with different + exclusions, or one that cannot be lined up (a needed figure not + published, or a figure both sides exclude with different signs), is + withheld for review; + - a report that breaks the format (``_small_cell_problems``) fails. + """ + + problems: list[str] = [] + review: list[str] = [] + # An empty or unfinished report measures nothing; it must never read as + # "no moves beyond the bounds". + for name, report in (("candidate", candidate), ("previous", previous)): + if report.get("status") != "ok": + problems.append(f"the {name} report status is {report.get('status')!r}") + if not report.get("metrics"): + problems.append(f"the {name} report has no metrics") + if not report.get("identities"): + problems.append(f"the {name} report has no checks") + # One row per check: a repeated id would let either copy decide. + check_counts = Counter( + str(row.get("identity_id")) for row in report.get("identities", []) + ) + repeated = sorted(check for check, count in check_counts.items() if count > 1) + if repeated: + problems.append( + f"the {name} report repeats checks: {', '.join(repeated[:8])}" + ) + # The previous report's statuses decide regressions and new warnings, so + # an unknown one there is no more a pass than in the candidate. + previous_unknown = [ + f"{row.get('identity_id')} ({row.get('status')!r})" + for row in previous.get("identities", []) + if row.get("status") not in CHECK_STATUSES + ] + if previous_unknown: + problems.append( + "the previous report has checks with an unknown status: " + + ", ".join(previous_unknown[:8]) + ) + previous_rows = { + str(row.get("identity_id")): row for row in previous.get("identities", []) + } + hard_failures: list[str] = [] + regressions: list[str] = [] + preexisting: list[str] = [] + new_warnings: list[str] = [] + warnings_: list[str] = [] + skipped: list[str] = [] + newly_skipped: list[str] = [] + hard_skipped: list[str] = [] + unknown_statuses: list[str] = [] + candidate_checks = set() + for row in candidate.get("identities", []): + check_id = str(row.get("identity_id")) + candidate_checks.add(check_id) + status = row.get("status") + earlier = previous_rows.get(check_id) or {} + before = earlier.get("status") + if status not in CHECK_STATUSES: + unknown_statuses.append(f"{check_id} ({status!r})") + elif status == "fail": + if classify_check(check_id) == "hard": + hard_failures.append(check_id) + elif before != "fail": + regressions.append(check_id) + elif _wider_failure(row, earlier): + regressions.append(f"{check_id} (fails in more scopes)") + else: + preexisting.append(check_id) + elif status == "warn": + warnings_.append(check_id) + if before != "warn": + new_warnings.append(check_id) + elif status == "skipped": + # Not run: never a pass, whatever the check's class. + skipped.append(check_id) + if classify_check(check_id) == "hard": + hard_skipped.append(check_id) + if before in ("pass", "warn", "fail"): + newly_skipped.append(check_id) + if unknown_statuses: + # A status this gate does not know is not a pass. + problems.append( + f"checks with an unknown status: {', '.join(unknown_statuses[:8])}" + ) + # A check the previous pair ran and the candidate did not: the tool and + # its definitions are the same (metrics_version), so coverage was lost. + lost_checks = sorted(set(previous_rows) - candidate_checks) + lost_hard = [ + check_id + for check_id in lost_checks + if classify_check(check_id) == "hard" + and candidate.get("partition_version") == previous.get("partition_version") + ] + if lost_hard: + problems.append(f"checks disappeared: {', '.join(lost_hard[:8])}") + elif lost_checks: + review.append(f"checks disappeared: {', '.join(lost_checks[:8])}") + if hard_failures: + problems.append(f"checks failed: {', '.join(hard_failures[:8])}") + if regressions: + problems.append( + f"checks the previous pair passed now fail: {', '.join(regressions[:8])}" + ) + if new_warnings: + review.append(f"checks newly warn: {', '.join(new_warnings[:8])}") + if newly_skipped: + review.append( + "checks the previous pair ran were not run: " + ", ".join(newly_skipped[:8]) + ) + if hard_skipped: + review.append(f"hard checks were not run: {', '.join(hard_skipped[:8])}") + if candidate.get("metrics_version") != previous.get("metrics_version"): + problems.append( + "metric definitions differ between the reports " + f"({previous.get('metrics_version')} → {candidate.get('metrics_version')})" + ) + if candidate.get("partition_version") != previous.get("partition_version"): + review.append("the model's fiscal partition changed") + current = {_metric_key(row): row for row in candidate.get("metrics", [])} + before_metrics = {_metric_key(row): row for row in previous.get("metrics", [])} + # Bounds are meaningful only when both sides contain usable values. + # In particular, NaN in the baseline must not silently skip a large move. + report_metric_rows = ( + ("candidate", candidate.get("metrics", [])), + ("previous", previous.get("metrics", [])), + ) + unsafe = False + for name, rows in report_metric_rows: + # A suppressed figure is published without a value by design. + nonfinite = [ + f"{row.get('metric_id')} {row.get('year')}" + for row in rows + if not _suppressed(row) and not _finite_number(row.get("value")) + ] + if nonfinite: + problems.append( + f"the {name} report has non-finite values: {', '.join(nonfinite[:8])}" + ) + format_problems = _small_cell_problems(name, rows) + problems.extend(format_problems) + unsafe = unsafe or bool(format_problems) + lost = sorted(f"{key[0]} {key[1]}" for key in set(before_metrics) - set(current)) + if lost: + problems.append(f"metrics disappeared: {', '.join(lost[:8])}") + without_baseline = sorted( + f"{key[0]} {key[1]}" for key in set(current) - set(before_metrics) + ) + if without_baseline: + review.append(f"metrics have no baseline: {', '.join(without_baseline[:8])}") + moves = [] + withheld: list[dict[str, Any]] = [] + aligned_totals: list[str] = [] + suppressed_on_both = 0 + # A report that breaks the small-cell format may carry a figure from + # fewer than 10 records: it fails above, and no value from either report + # enters the evidence, which the public ledger publishes. + comparable = {} if unsafe else current + # Years sort as text so a malformed year (failed above) cannot crash this. + for key, row in sorted( + comparable.items(), key=lambda item: (item[0][0], str(item[0][1])) + ): + old = before_metrics.get(key) + if old is None: + continue + if _suppressed(row) and _suppressed(old): + suppressed_on_both += 1 + continue + if _suppressed(row) or _suppressed(old): + # A figure from fewer than 10 records on one side: so is its change. + withheld.append( + { + "metric_id": key[0], + "year": key[1], + "reason": "suppressed in the " + + ("candidate" if _suppressed(row) else "previous") + + " report", + } + ) + continue + if not (_finite_number(row.get("value")) and _finite_number(old.get("value"))): + continue + new_value, old_value = float(row["value"]), float(old["value"]) + new_compared, old_compared = new_value, old_value + aligned_on: Optional[list[str]] = None + # Both reports passed the format check, so their signs are valid. + new_signs, old_signs = _valid_signs(row) or {}, _valid_signs(old) or {} + if new_signs != old_signs: + # Each side leaves out its own suppressed figures, each with its + # sign (the tool compares the signed maps, whose keys are the + # excluded ids). Subtracting the totals as published would mix + # definitions and could give an excluded figure back, so they are + # lined up first, from published figures only, or withheld. + reason = None + if any( + new_signs[metric_id] != old_signs[metric_id] + for metric_id in set(new_signs) & set(old_signs) + ): + reason = ( + "a figure both reports leave out enters their totals " + "with different signs" + ) + elif not (_additive(row) and _additive(old)): + reason = "the reports leave out different suppressed figures" + else: + lined_up = ( + _aligned(new_value, row, old_signs, current, key[1]), + _aligned(old_value, old, new_signs, before_metrics, key[1]), + ) + if lined_up[0] is None or lined_up[1] is None: + reason = ( + "the reports leave out different suppressed figures, " + "and one needed to line them up is not published" + ) + else: + new_compared, old_compared = lined_up + aligned_on = sorted(set(new_signs) | set(old_signs)) + aligned_totals.append(f"{key[0]} {key[1]}") + if reason is not None: + withheld.append( + { + "metric_id": key[0], + "year": key[1], + "reason": reason, + "candidate_excludes": list(_excludes(row)), + "previous_excludes": list(_excludes(old)), + } + ) + continue + relative, absolute = _bound(bounds, row.get("kind")) + # The relative part is taken on the previous published total, as the + # tool's flag threshold is. + threshold = max(relative * abs(old_value), absolute) + change = new_compared - old_compared + if abs(change) > threshold: + move = { + "metric_id": key[0], + "year": key[1], + "kind": row.get("kind"), + "previous": old_value, + "candidate": new_value, + "change": change, + "threshold": threshold, + } + if aligned_on is not None: + move["lined_up_without"] = aligned_on + moves.append(move) + if moves: + review.append(f"{len(moves)} metric move(s) exceed their review bounds") + if withheld: + review.append( + f"{len(withheld)} metric change(s) are withheld because a figure " + "from fewer than 10 records is suppressed on one side, or the two " + "sides leave out different ones and cannot be lined up" + ) + evidence = { + "metrics_version": candidate.get("metrics_version"), + "partition_version": candidate.get("partition_version"), + "previous_partition_version": previous.get("partition_version"), + "hard_check_failures": hard_failures, + "regressed_checks": regressions, + "preexisting_check_failures": preexisting, + "warn_identities": warnings_, + "new_warnings": new_warnings, + "skipped_checks": skipped, + "newly_skipped_checks": newly_skipped, + "hard_checks_not_run": hard_skipped, + "unknown_status_checks": unknown_statuses, + "lost_checks": lost_checks, + "metrics_without_baseline": without_baseline, + "suppressed_metrics": sorted( + f"{key[0]} {key[1]}" for key, row in current.items() if _suppressed(row) + ), + "suppressed_on_both_sides": suppressed_on_both, + "withheld_changes": withheld, + "lined_up_totals": aligned_totals, + # True when a report breaks the small-cell format: no value from + # either report was compared or recorded. + "values_withheld": unsafe, + "moves_beyond_bounds": moves, + "candidate_report_sha256": evidence_sha256(candidate), + "previous_report_sha256": evidence_sha256(previous), + } + if problems: + return "fail", "; ".join(problems), evidence + if review: + return "needs_review", "; ".join(review), evidence + notes = [] + if suppressed_on_both: + notes.append(f"{suppressed_on_both} suppressed on both sides, not compared") + if preexisting: + notes.append(f"{len(preexisting)} known check failures carried over") + note = f" ({'; '.join(notes)})" if notes else "" + compared = len(current) - suppressed_on_both + return "pass", f"{compared} metrics within their review bounds{note}.", evidence + + +def metrics_review( + *, + candidate_report: Optional[Mapping[str, Any]], + previous_report: Optional[Mapping[str, Any]], + bounds: Mapping[str, Any], + cross_report: Optional[Mapping[str, Any]] = None, + unavailable_reason: Optional[str] = None, +) -> GateOutcome: + """Compare metrics against the previous certificate under review bounds. + + With ``cross_report`` (the candidate model on the previous data), the + change splits into a model-only step (cross against previous) and a + data-only step (candidate against cross); the gate takes the worse. + """ + + if unavailable_reason or candidate_report is None or previous_report is None: + return _result( + "metrics_review", + "unavailable", + unavailable_reason or "Metric reports for both pairs are not available.", + {}, + ) + order = {"pass": 0, "needs_review": 1, "fail": 2} + steps: dict[str, tuple[str, str, Evidence]] = {} + if cross_report is not None: + steps["model_only"] = compare_metric_reports( + cross_report, previous_report, bounds + ) + steps["data_only"] = compare_metric_reports( + candidate_report, cross_report, bounds + ) + else: + steps["total"] = compare_metric_reports( + candidate_report, previous_report, bounds + ) + worst = max(steps.values(), key=lambda step: order[step[0]]) + evidence = {name: step[2] for name, step in steps.items()} + summary = "; ".join(f"{name}: {step[1]}" for name, step in steps.items()) + return _result( + "metrics_review", + worst[0], + summary, + evidence, + details={name: step[0] for name, step in steps.items()}, + ) + + +def publishable_metrics_report(report: Mapping[str, Any]) -> dict[str, Any]: + """A metrics report as it may be written to the qualification ledger. + + The ledger is a branch of a public repository. A report that breaks the + small-cell format (``_small_cell_problems``) may carry a figure from + fewer than 10 records, so only its digest and its problems, which name + figures but give no values, are kept. A report the tool could not finish + has no metrics to check and is kept as it is. + """ + + problems = _small_cell_problems("metrics", report.get("metrics") or []) + if not problems: + return dict(report) + return { + "withheld": ( + "This report breaks the metrics tool's small-cell format, so it " + "is not published." + ), + "report_sha256": evidence_sha256(report), + "problems": problems, + } + + +# --- Reform responsiveness ------------------------------------------------------- + + +def _fiscal_year(year: int) -> str: + return f"{year}-{(year + 1) % 100:02d}" + + +def reform_responsiveness( + *, + report: Optional[Mapping[str, Any]], + standard_reforms: Sequence[Mapping[str, Any]], + years: Sequence[int], + unavailable_reason: Optional[str] = None, +) -> GateOutcome: + """Every standard reform ran and moved its target as required, or fail. + + The metrics tool costs its standard reforms and, for each, checks that + the weighted total of the reform's target variable moved in the stated + direction by at least the stated minimum in every year + (``responsive:``), and that the reform ran (``reform_runs:``). + This gate holds the candidate's report to that: + + - both checks must be present and ``pass`` for every reform in + ``standard_reforms`` (the tool's own list); a missing check is a + failure, so a run that costed no reforms cannot pass; + - the report must cover exactly ``years``; + - the tool's verdict is recomputed here from the report's + ``reform_targets.`` values, and a disagreement is an error + (differential check of the check). + + Any reform that does not respond fails the gate: a reform the model + silently ignores is a defect, never a small number. The one review path + is a year the tool withholds (``reform_targets.`` published as + suppressed, because the change would come from fewer than 10 records): + there the tool's verdict, which it takes from the withheld value, cannot + be recomputed, so a passing ``responsive`` check needs review rather + than passing; a failing one still fails. The gate is monotone: shrinking + a reform's response toward zero or flipping its sign never turns a + failure into a pass. + """ + + if unavailable_reason or report is None: + return _result( + "reform_responsiveness", + "unavailable", + unavailable_reason or "No candidate metrics report is available.", + {}, + ) + if not standard_reforms: + return _result( + "reform_responsiveness", + "unavailable", + "The metrics tool defines no standard reforms.", + {}, + ) + checks = { + str(row.get("identity_id")): row for row in report.get("identities") or [] + } + rows = { + (str(row.get("metric_id")), row.get("year")): row + for row in report.get("metrics") or [] + } + problems: list[str] = [] + disagreements: list[str] = [] + unverifiable: list[str] = [] + table: list[dict[str, Any]] = [] + if report.get("status") not in (None, "ok"): + problems.append(f"the report status is {report.get('status')!r}") + if sorted(report.get("years") or []) != sorted(years): + disagreements.append( + f"the report covers {report.get('years')}, not the horizon {list(years)}" + ) + # A repeated target or check would let either copy decide. + repeated = sorted( + f"{key[0]} {key[1]}" + for key, count in Counter( + (str(row.get("metric_id")), row.get("year")) + for row in report.get("metrics") or [] + ).items() + if count > 1 and key[0].startswith("reform_targets.") + ) + sorted( + check + for check, count in Counter( + str(row.get("identity_id")) for row in report.get("identities") or [] + ).items() + if count > 1 and check.startswith(("reform_runs:", "responsive:")) + ) + if repeated: + disagreements.append(f"the report repeats {', '.join(repeated[:8])}") + # A report that breaks the small-cell format may carry a figure from + # fewer than 10 records: no value from it enters the evidence. + format_problems = _small_cell_problems("candidate", report.get("metrics") or []) + disagreements.extend(format_problems) + malformed: list[str] = [] + for reform in standard_reforms: + reform_id = reform["id"] + sign = 1.0 if reform["direction"] == "up" else -1.0 + changes: dict[str, Any] = {} + withheld_years: list[int] = [] + # True: every year meets the minimum; False: a published year does + # not; None: every published year does, but a year is withheld. + recomputed: Optional[bool] = True + for year in years: + target = rows.get((f"reform_targets.{reform_id}", year)) or {} + value = target.get("value") + if _suppressed(target): + # Never copied into evidence, whatever the row carries. + changes[str(year)] = None + if value is None: + withheld_years.append(year) + else: + malformed.append(f"reform_targets.{reform_id} {year}") + recomputed = False + continue + changes[str(year)] = None if format_problems else value + if ( + isinstance(value, bool) + or not isinstance(value, (int, float)) + or not math.isfinite(value) + or not sign * value >= reform["minimum"] + ): + recomputed = False + if recomputed and withheld_years: + recomputed = None + ran = (checks.get(f"reform_runs:{reform_id}") or {}).get("status") + responsive = checks.get(f"responsive:{reform_id}") or {} + row = { + **reform, + "target_changes": changes, + "withheld_years": withheld_years, + "reform_runs": ran, + "responsive": responsive.get("status"), + # The tool's text can quote amounts; none from an unsafe report. + "detail": None if format_problems else responsive.get("detail"), + } + table.append(row) + label = f"{reform['name']} ({reform_id})" + if ran != "pass": + problems.append(f"{label} did not run") + elif responsive.get("status") != "pass": + problems.append( + f"{label} did not respond: " + + ( + row["detail"] + or ( + "its detail is withheld" + if format_problems and responsive + else "no responsive check in the report" + ) + ) + ) + if ran == "pass" and responsive.get("status") in ("pass", "fail"): + passed = responsive["status"] == "pass" + if recomputed is None: + # A failing verdict may come from a withheld year; a passing + # one cannot be checked there. + if passed: + unverifiable.append( + f"{label}: the tool's responsive check passes but " + + ", ".join(_fiscal_year(year) for year in withheld_years) + + " are withheld (fewer than 10 records), so it cannot " + "be recomputed there" + ) + elif passed != recomputed: + disagreements.append( + f"{label}: the tool's responsive check is " + f"{responsive['status']} but its reform_targets values " + f"{'meet' if recomputed else 'do not meet'} the minimum" + ) + if malformed: + disagreements.append( + "suppressed reform targets carry a value: " + ", ".join(malformed[:8]) + ) + evidence = { + "years": list(years), + "reforms": table, + "candidate_report_sha256": evidence_sha256(report), + } + if problems: + return _result( + "reform_responsiveness", + "fail", + f"{len(problems)} responsiveness failure(s): " + "; ".join(problems[:6]), + evidence, + ) + if disagreements: + return _result( + "reform_responsiveness", "error", "; ".join(disagreements[:6]), evidence + ) + if unverifiable: + return _result( + "reform_responsiveness", + "needs_review", + "; ".join(unverifiable[:6]), + evidence, + ) + return _result( + "reform_responsiveness", + "pass", + f"All {len(table)} standard reforms ran and moved their targets by at " + f"least their minimums in every year from {_fiscal_year(years[0])} to " + f"{_fiscal_year(years[-1])}.", + evidence, + ) + + +# --- Metrics tool (W3's aggregate-impact report) --------------------------------- + +VENDORED_TOOL_PACKAGE = "pe_uk_aggregate_impact" +PARTITION_PATH_IN_REPO = "policyengine_uk/data/fiscal_partition.yaml" + +#: Self-contained runner, executed in any environment that has a policyengine +#: release installed (the candidate's, or the previous certified pair's +#: throwaway venv). Arguments: vendor root, JSON settings, output path. +METRICS_RUNNER = """ +import importlib, importlib.util, json, sys +from pathlib import Path +vendor, settings, out = sys.argv[1], json.loads(sys.argv[2]), Path(sys.argv[3]) +sys.path.insert(0, vendor) +api = importlib.import_module("pe_uk_aggregate_impact.api") +partition_module = importlib.import_module("pe_uk_aggregate_impact.partition") +installed = partition_module.default_partition_path() +if installed is not None: + partition_path, source = installed, "installed policyengine_uk" +else: + partition_path, source = Path(vendor) / "fiscal_partition.yaml", "vendored tool commit" +dataset = settings.get("dataset_path") +if not dataset: + from policyengine.provenance.dataset_materialization import materialize_dataset + dataset = materialize_dataset("uk", data_dir=Path(settings["data_dir"])).path +from policyengine_uk import Microsimulation +# No ``reforms`` argument: the tool then costs its own standard set and checks +# each reform's responsiveness (passing [] would cost none). +report = api.compute_budget_metrics( + Microsimulation(dataset=str(dataset)), + years=settings["years"], + partition=partition_module.load_partition(partition_path), + run_guard=settings.get("run_guard", True), + label=settings.get("label", "candidate"), +) +report["partition_source"] = source +report["dataset_path"] = str(dataset) +out.write_text(json.dumps(report)) +""" + + +#: Runner for a pair's input coverage and supported years, in its own +#: environment. It mirrors ``budget_inputs`` and ``supported_years``; a +#: differential test checks they agree. Arguments: JSON settings, output path. +INPUTS_RUNNER = """ +import json, sys +from pathlib import Path +import pandas as pd +settings, out = json.loads(sys.argv[1]), Path(sys.argv[2]) +path = settings.get("dataset_path") +if not path: + from policyengine.provenance.dataset_materialization import materialize_dataset + path = str(materialize_dataset("uk", data_dir=Path(settings["data_dir"])).path) +inputs = settings.get("model_input_variables") +if inputs is None: + from policyengine_uk.system import system + inputs = [name for name, variable in system.variables.items() if variable.is_input_variable()] +inputs = set(inputs) +with pd.HDFStore(path, mode="r") as store: + tables = {e: store[e] for e in ("person", "benunit", "household") if f"/{e}" in store} + first_year = int(str(store["time_period"].iloc[0])[:4]) +columns = {} +for entity, table in tables.items(): + for column in table.columns: + columns.setdefault(column, entity) +def degenerate(series): + values = series.dropna() + if len(values) == 0 or values.nunique() <= 1: + return True + if values.dtype.kind in "biuf": + return bool((values.astype(float) == 0).all()) + return False +unusable = [] +for name in settings["critical"]: + if name not in columns: + unusable.append([name, "missing"]) + elif degenerate(tables[columns[name]][name]): + unusable.append([name, "degenerate"]) +years = settings.get("projected_years") +if years is None: + from policyengine_uk.data.dataset_schema import UKSingleYearDataset + from policyengine_uk.data.economic_assumptions import extend_single_year_dataset + from policyengine_uk.system import system + multi_year = extend_single_year_dataset(UKSingleYearDataset(path), system.parameters) + years = getattr(multi_year, "years", None) + years = years if years is not None else list(multi_year.datasets) +years = sorted(int(y) for y in years) +out.write_text(json.dumps({ + "dataset_path": path, + "missing_inputs": sorted(inputs - set(columns)), + "unusable_critical": unusable, + "supported_years": [y for y in years if y >= first_year], +})) +""" + + +def run_inputs_snapshot( + python: str, + *, + critical: Sequence[str], + out: Path, + dataset_path: Optional[Path] = None, + data_dir: Optional[Path] = None, + model_input_variables: Optional[Sequence[str]] = None, + projected_years: Optional[Sequence[int]] = None, + run: Optional[Callable[..., Any]] = None, +) -> dict[str, Any]: + """Input coverage and supported years of a pair, via ``INPUTS_RUNNER``. + + ``model_input_variables`` and ``projected_years`` default to what the + environment's policyengine-uk reports; tests pass them explicitly. + """ + + import subprocess + + run = run if run is not None else subprocess.run + settings = { + "critical": list(critical), + "dataset_path": str(dataset_path) if dataset_path else None, + "data_dir": str(data_dir) if data_dir else None, + "model_input_variables": ( + list(model_input_variables) if model_input_variables is not None else None + ), + "projected_years": list(projected_years) + if projected_years is not None + else None, + } + run( + [python, "-c", INPUTS_RUNNER, json.dumps(settings), str(out)], + check=True, + cwd=str(out.parent), + ) + snapshot: dict[str, Any] = json.loads(out.read_text()) + return snapshot + + +def vendor_metrics_tool( + tool: Mapping[str, Any], destination: Path, *, get: Callable[..., Any] +) -> Path: + """Copy the metrics tool and its partition from one policyengine-uk commit. + + Returns the directory to put on ``sys.path``; the tool imports as + ``pe_uk_aggregate_impact``. The commit is pinned in the qualification + config so both pairs are measured with the same definitions. + """ + + import tarfile + + repository = tool["repository"] + commit = tool["commit"] + tool_path = tool["path"].rstrip("/") + root = destination / f"metrics-tool-{commit[:12]}" + package = root / VENDORED_TOOL_PACKAGE + if (package / "api.py").exists(): + return root + response = get( + f"https://codeload.github.com/{repository}/tar.gz/{commit}", timeout=120 + ) + response.raise_for_status() + package.mkdir(parents=True, exist_ok=True) + with tarfile.open(fileobj=io.BytesIO(response.content), mode="r:gz") as archive: + for member in archive.getmembers(): + _, _, relative = member.name.partition("/") + if not member.isfile(): + continue + if relative.startswith(tool_path + "/"): + target = package / relative[len(tool_path) + 1 :] + elif relative == PARTITION_PATH_IN_REPO: + target = root / "fiscal_partition.yaml" + else: + continue + if ".." in Path(relative).parts: + raise ValueError(f"Unsafe path in the tool archive: {member.name}") + target.parent.mkdir(parents=True, exist_ok=True) + extracted = archive.extractfile(member) + assert extracted is not None + target.write_bytes(extracted.read()) + if not (package / "api.py").exists(): + raise ValueError(f"{repository}@{commit} has no {tool_path}/api.py.") + return root + + +def load_standard_reforms(vendor_root: Path, filename: str) -> list[dict[str, Any]]: + """The metrics tool's standard reform set, read from the vendored tool. + + Each entry must carry an ``id`` and a ``responds`` block (``variable``, + ``direction`` ``up`` or ``down``, positive ``minimum``): a standard reform + with no stated response cannot be held to one, so it is refused. + """ + + import yaml # type: ignore[import-untyped] + + path = vendor_root / VENDORED_TOOL_PACKAGE / filename + if not path.exists(): + raise ValueError(f"The metrics tool ships no {filename}.") + document = yaml.safe_load(path.read_text()) or {} + reforms: list[dict[str, Any]] = [] + for entry in document.get("reforms") or []: + responds = entry.get("responds") or {} + minimum = responds.get("minimum") + if ( + not entry.get("id") + or not responds.get("variable") + or responds.get("direction") not in ("up", "down") + or isinstance(minimum, bool) + or not isinstance(minimum, (int, float)) + or not minimum > 0 + ): + raise ValueError( + f"Standard reform {entry.get('id') or entry.get('name')!r} states " + "no id or no expected response." + ) + reforms.append( + { + "id": str(entry["id"]), + "name": str(entry.get("name") or entry["id"]), + "variable": str(responds["variable"]), + "direction": responds["direction"], + "minimum": float(minimum), + } + ) + ids = [reform["id"] for reform in reforms] + if len(set(ids)) != len(ids): + raise ValueError("The standard reform set repeats an id.") + return reforms + + +def run_metrics_report( + python: str, + vendor_root: Path, + *, + years: Sequence[int], + out: Path, + dataset_path: Optional[Path] = None, + data_dir: Optional[Path] = None, + run_guard: bool = True, + label: str = "candidate", + run: Optional[Callable[..., Any]] = None, +) -> dict[str, Any]: + """Compute a metrics report with ``python`` (any environment) via the runner. + + The tool also costs its standard reform set against the same baseline, + each reform as a full run over ``years``. + """ + + import subprocess + + run = run if run is not None else subprocess.run + settings = { + "years": list(years), + "dataset_path": str(dataset_path) if dataset_path else None, + "data_dir": str(data_dir) if data_dir else None, + "run_guard": run_guard, + "label": label, + } + run( + [ + python, + "-c", + METRICS_RUNNER, + str(vendor_root), + json.dumps(settings), + str(out), + ], + check=True, + cwd=str(out.parent), + ) + report: dict[str, Any] = json.loads(out.read_text()) + return report + + +class PreviousPairUnavailable(ValueError): + """The exact certified baseline cannot be constructed or verified.""" + + +def _previous_pair_pins( + previous: Mapping[str, Any], ledger_root: Optional[Path] = None +) -> dict[str, Any]: + """Certificate pins, completed by its exact ledger receipt when available.""" + + pins = dict(previous) + pair_id = previous.get("qualification_pair_id") + if ledger_root is not None and pair_id: + try: + receipt = QualificationReceipt.model_validate_json( + ( + ledger_root / "uk" / pair_id / "qualification_receipt.json" + ).read_text() + ) + except (OSError, ValueError) as exc: + raise PreviousPairUnavailable( + "The previous pair's ledger receipt is unavailable." + ) from exc + pair = receipt.pair + expected = { + "model_version": pair.model.version, + "core_version": pair.core.version, + "data_release_version": pair.data.release_version, + "dataset_sha256": pair.data.dataset_sha256, + "model_wheel_sha256": pair.model.wheel_sha256, + "core_wheel_sha256": pair.core.wheel_sha256, + } + if ( + not receipt.promotable + or receipt.pair_id != pair_id + or any( + previous.get(key) is not None and previous[key] != value + for key, value in expected.items() + ) + ): + raise PreviousPairUnavailable( + "The previous certificate and ledger name different pairs." + ) + reference = { + "repo_id": pair.data.repo_id, + "repo_type": pair.data.repo_type, + "path": pair.data.dataset_path, + "revision": pair.data.dataset_revision, + "sha256": pair.data.dataset_sha256, + } + recorded_reference = previous.get("dataset_reference") + if recorded_reference and any( + recorded_reference.get(key) != value for key, value in reference.items() + ): + raise PreviousPairUnavailable( + "The previous dataset reference differs from its ledger pair." + ) + pins.update( + expected, + dataset_reference=reference, + dataset_name=pair.data.dataset, + release_manifest_reference={ + "repo_id": pair.data.repo_id, + "repo_type": pair.data.repo_type, + "path": pair.data.release_manifest_path, + "revision": pair.data.release_manifest_revision, + "sha256": pair.data.release_manifest_sha256, + }, + ) + for key in ( + "bundle_version", + "model_version", + "core_version", + "data_release_version", + "dataset_sha256", + ): + if not isinstance(pins.get(key), str) or not pins[key]: + raise PreviousPairUnavailable(f"The previous certificate has no {key} pin.") + reference = pins.get("dataset_reference") + if ( + not isinstance(reference, Mapping) + or any( + not isinstance(reference.get(key), str) or not reference[key] + for key in ("repo_id", "path", "revision") + ) + or reference.get("repo_type") not in ("model", "dataset") + or reference.get("sha256") != pins["dataset_sha256"] + or not re.fullmatch(r"[a-f0-9]{64}", pins["dataset_sha256"]) + ): + raise PreviousPairUnavailable( + "The previous certificate has no exact dataset reference." + ) + return pins + + +# This verification subprocess uses the current verifier, independently of +# whether the older published wrapper included it. Its namespace is isolated +# from the later metrics subprocess, which uses the installed previous wrapper. +PREVIOUS_IDENTITY_RUNNER = """ +import importlib.metadata as metadata, json, sys, types +from pathlib import Path +settings, out = json.loads(sys.argv[1]), Path(sys.argv[2]) +package = types.ModuleType("policyengine") +package.__path__ = [settings["source"]] +sys.modules["policyengine"] = package +from policyengine.provenance.wheels import verify_installed_against_wheel +problems = [] +for wheel in settings["wheels"]: + name = wheel["name"] + try: + if metadata.version(name) != wheel["version"]: + problems.append(name + ": installed version differs from the previous certificate") + evidence = verify_installed_against_wheel(name, Path(wheel["path"])) + if evidence["wheel_sha256"] != wheel["sha256"]: + problems.append(name + ": wheel hash differs from the previous certificate") + problems.extend(evidence["problems"]) + except (metadata.PackageNotFoundError, OSError, ValueError) as exc: + problems.append(name + ": cannot verify installation: " + str(exc)) +out.write_text(json.dumps({"problems": problems})) +""" + + +def _construct_previous_pair_python( + previous: Mapping[str, Any], + work_dir: Path, + *, + ledger_root: Optional[Path] = None, + run: Optional[Callable[..., Any]] = None, +) -> str: + """Install and verify the certificate's wheels, independently of wrapper extras.""" + + import subprocess + + import requests + + from policyengine.provenance.uk_qualification import _wheel + + run = run if run is not None else subprocess.run + pins = _previous_pair_pins(previous, ledger_root) + wheels = [] + for name, kind in (("policyengine-uk", "model"), ("policyengine-core", "core")): + version = pins[f"{kind}_version"] + response = requests.get( + f"https://pypi.org/pypi/{name}/{version}/json", timeout=30 + ) + response.raise_for_status() + wheel = _wheel(response.json()["urls"], name, version) + expected_sha256 = pins.get(f"{kind}_wheel_sha256") or wheel.sha256 + if wheel.sha256 != expected_sha256: + raise PreviousPairUnavailable( + f"{name}: PyPI wheel differs from the previous certificate." + ) + path = _download( + wheel.url, + work_dir / "previous-wheels" / expected_sha256 / wheel.filename, + get=requests.get, + ) + if sha256_file(path) != expected_sha256: + raise PreviousPairUnavailable( + f"{name}: downloaded wheel differs from the previous certificate." + ) + wheels.append( + { + "name": name, + "version": version, + "sha256": expected_sha256, + "path": str(path), + } + ) + # Same wrapper version can certify several pairs during its publication window. + # The environment cache must include the resolved wheel and dataset pins. + venv = ( + work_dir / f"previous-{content_sha256({'pins': pins, 'wheels': wheels})[:24]}" + ) + python = venv / "bin" / "python" + if not python.exists(): + run(["uv", "venv", str(venv), "--python", "3.13"], check=True) + run( + [ + "uv", + "pip", + "install", + "--python", + str(python), + f"policyengine=={pins['bundle_version']}", + *(wheel["path"] for wheel in wheels), + ], + check=True, + ) + output = work_dir / "previous-identity.json" + output.unlink(missing_ok=True) + run( + [ + str(python), + "-c", + PREVIOUS_IDENTITY_RUNNER, + json.dumps( + { + "source": str(Path(__file__).resolve().parents[1]), + "wheels": wheels, + } + ), + str(output), + ], + check=True, + ) + identity = json.loads(output.read_text()) + if not isinstance(identity, Mapping) or identity.get("problems") != []: + raise PreviousPairUnavailable( + f"The installed previous pair does not match its pins: {identity}." + ) + return str(python) + + +def _materialize_previous_pair_dataset( + previous: Mapping[str, Any], work_dir: Path, *, ledger_root: Optional[Path] = None +) -> Path: + """Materialize the previous certificate's exact data, bypassing wrapper defaults.""" + + import os + + import requests + + pins = _previous_pair_pins(previous, ledger_root) + reference = pins["dataset_reference"] + path = ( + work_dir + / "previous-data" + / pins["dataset_sha256"] + / Path(reference["path"]).name + ) + if not path.exists(): + token = os.environ.get("HUGGING_FACE_TOKEN") or os.environ.get("HF_TOKEN") + url = _hf_url( + reference["repo_id"], + reference["repo_type"], + reference["revision"], + reference["path"], + ) + _download( + url, + path, + get=lambda *args, **kwargs: requests.get( + *args, + **kwargs, + headers={"Authorization": f"Bearer {token}"} if token else {}, + ), + ) + if sha256_file(path) != pins["dataset_sha256"]: + raise PreviousPairUnavailable( + "The previous dataset bytes differ from the certificate." + ) + manifest_reference = pins.get("release_manifest_reference") or {} + if ( + any( + not isinstance(manifest_reference.get(key), str) + or not manifest_reference[key] + for key in ("repo_id", "path", "revision") + ) + or manifest_reference.get("repo_type") not in ("model", "dataset") + or not pins.get("dataset_name") + ): + raise PreviousPairUnavailable( + "The previous data release has no pinned manifest reference." + ) + token = os.environ.get("HUGGING_FACE_TOKEN") or os.environ.get("HF_TOKEN") + response = requests.get( + _hf_url( + manifest_reference["repo_id"], + manifest_reference["repo_type"], + manifest_reference["revision"], + manifest_reference["path"], + ), + headers={"Authorization": f"Bearer {token}"} if token else {}, + timeout=30, + ) + response.raise_for_status() + if ( + manifest_reference.get("sha256") + and hashlib.sha256(response.content).hexdigest() != manifest_reference["sha256"] + ): + raise PreviousPairUnavailable( + "The previous release manifest bytes differ from the ledger." + ) + manifest = DataReleaseManifest.model_validate_json(response.content) + artifact = manifest.artifacts.get(pins["dataset_name"]) + if ( + manifest.data_package.version != pins["data_release_version"] + or manifest.default_datasets.get("national") != pins["dataset_name"] + or artifact is None + or artifact.sha256 != pins["dataset_sha256"] + or artifact.path != reference["path"] + or artifact.revision != reference["revision"] + or artifact.repo_id != reference["repo_id"] + or (artifact.repo_type or manifest_reference["repo_type"]) + != reference["repo_type"] + ): + raise PreviousPairUnavailable( + "The previous release manifest does not match the certified data pair." + ) + return path + + +def previous_pair_python( + previous: Mapping[str, Any], + work_dir: Path, + *, + ledger_root: Optional[Path] = None, + run: Optional[Callable[..., Any]] = None, +) -> str: + """Construct a verified baseline, or report that it is unavailable.""" + + import subprocess + + import requests + + try: + return _construct_previous_pair_python( + previous, work_dir, ledger_root=ledger_root, run=run + ) + except PreviousPairUnavailable: + raise + except ( + OSError, + ValueError, + KeyError, + TypeError, + subprocess.CalledProcessError, + requests.RequestException, + ) as exc: + raise PreviousPairUnavailable( + f"The exact previous environment could not be verified: {exc}" + ) from exc + + +def previous_pair_dataset( + previous: Mapping[str, Any], work_dir: Path, *, ledger_root: Optional[Path] = None +) -> Path: + """Materialize verified previous data, or report that it is unavailable.""" + + import requests + + try: + return _materialize_previous_pair_dataset( + previous, work_dir, ledger_root=ledger_root + ) + except PreviousPairUnavailable: + raise + except (OSError, ValueError, KeyError, TypeError, requests.RequestException) as exc: + raise PreviousPairUnavailable( + f"The exact previous data could not be verified: {exc}" + ) from exc + + +# --- Orchestration ------------------------------------------------------------- + + +def run_gate(gate_id: str, function: Callable[[], GateOutcome]) -> GateOutcome: + """Run one gate; an unexpected exception becomes an ``error`` result.""" + + try: + return function() + except PreviousPairUnavailable as exc: + return _result( + gate_id, "unavailable", str(exc), {"previous_pair_unavailable": str(exc)} + ) + except Exception as exc: + return _result( + gate_id, + "error", + f"The gate raised {type(exc).__name__}: {exc}", + {"traceback": traceback.format_exc()}, + ) + + +def run_gates( + gates: Sequence[tuple[str, Callable[[], GateOutcome]]], +) -> tuple[list[GateResult], dict[str, Evidence]]: + """Run gates in order; always one result per gate, never fewer.""" + + results: list[GateResult] = [] + evidence: dict[str, Evidence] = {} + for gate_id, function in gates: + result, item = run_gate(gate_id, function) + if result.gate_id != gate_id: + result = result.model_copy(update={"gate_id": gate_id}) + # Orchestration may append source/tool metadata to a gate's evidence. + # Bind the result to the complete JSON that will actually be stored. + item = _json_safe(item) + result = result.model_copy(update={"evidence_sha256": evidence_sha256(item)}) + results.append(result) + evidence[gate_id] = item + return results, evidence + + +def _download(url: str, destination: Path, *, get: Callable[..., Any]) -> Path: + destination.parent.mkdir(parents=True, exist_ok=True) + with get(url, stream=True, timeout=120) as response: + response.raise_for_status() + with destination.open("wb") as stream: + for chunk in response.iter_content(chunk_size=1 << 20): + stream.write(chunk) + return destination + + +def _previous_gate_evidence( + ledger_root: Optional[Path], previous: Optional[Mapping[str, Any]] +) -> dict[str, Any]: + """Verified evidence of the exact certified pair and receipt's attempt. + + Legacy certificates that cannot bind every wheel and data pin by pair ID, + or incomplete/corrupt evidence, fall back to recomputation by the caller. + """ + + if ledger_root is None or not previous: + return {} + pair_id = previous.get("qualification_pair_id") + if not isinstance(pair_id, str) or not re.fullmatch(r"[a-f0-9]{64}", pair_id): + return {} + receipt_path = ledger_root / "uk" / pair_id / "qualification_receipt.json" + try: + receipt = QualificationReceipt.model_validate_json(receipt_path.read_text()) + pair = receipt.pair + if not receipt.promotable or receipt.pair_id != pair_id: + return {} + expected = ( + (pair.model.version, previous.get("model_version")), + (pair.core.version, previous.get("core_version")), + (pair.data.release_version, previous.get("data_release_version")), + (pair.data.dataset_sha256, previous.get("dataset_sha256")), + ) + if any(actual != certified for actual, certified in expected): + return {} + # The pair ID binds all wheels and data pins even for certificates + # whose bundle's package record does not repeat the core wheel hash. + for field, actual in ( + ("model_wheel_sha256", pair.model.wheel_sha256), + ("core_wheel_sha256", pair.core.wheel_sha256), + ): + if previous.get(field) is not None and previous[field] != actual: + return {} + for attempt_receipt in sorted( + (receipt_path.parent / "attempts").glob("*/qualification_receipt.json") + ): + attempt = QualificationReceipt.model_validate_json( + attempt_receipt.read_text() + ) + # Review acceptance can change the latest receipt without changing + # the immutable attempt's underlying qualification. + if attempt.unreviewed_sha256 != receipt.unreviewed_sha256: + continue + harness = json.loads((attempt_receipt.parent / "harness.json").read_text()) + if harness != receipt.harness.canonical_payload(): + continue + by_gate = {gate.gate_id: gate for gate in receipt.gates} + found: dict[str, Any] = {} + for name in ("supported_years", "budget_inputs"): + gate = by_gate.get(name) + evidence = json.loads( + (attempt_receipt.parent / "gates" / f"{name}.json").read_text() + ) + if ( + not gate + or gate.status not in ("pass", "needs_review") + or gate.evidence_sha256 != evidence_sha256(evidence) + ): + return {} + found[name] = evidence + if _valid_previous_snapshot_evidence(found): + return found + return {} + except (OSError, ValueError, TypeError, AttributeError): + # An unavailable cache is not qualification evidence; recompute it. + return {} + return {} + + +def _valid_previous_snapshot_evidence(evidence: Mapping[str, Any]) -> bool: + years = evidence["supported_years"].get("supported_years") + inputs = evidence["budget_inputs"] + missing = inputs.get("missing_inputs") + unusable = inputs.get("unusable_critical") + return ( + isinstance(years, list) + and bool(years) + and all( + isinstance(year, int) and not isinstance(year, bool) and year > 0 + for year in years + ) + and len(years) == len(set(years)) + and isinstance(missing, list) + and all(isinstance(name, str) and bool(name) for name in missing) + and len(missing) == len(set(missing)) + and isinstance(unusable, list) + and all( + isinstance(row, list) + and len(row) == 2 + and all(isinstance(value, str) and bool(value) for value in row) + for row in unusable + ) + ) + + +def run_candidate_gates( + resolved: Any, + harness: QualificationHarness, + *, + config: Mapping[str, Any], + work_dir: Path, + previous_certificate: Optional[Mapping[str, Any]], + ledger_root: Optional[Path], +) -> tuple[list[GateResult], dict[str, Evidence]]: + """Run gates 1–9 on the installed candidate, with real I/O.""" + + import os + + import requests + + from policyengine.provenance.dataset_materialization import materialize_dataset + + pair: CandidatePair = resolved.pair + work_dir.mkdir(parents=True, exist_ok=True) + data_dir = Path(config.get("data_dir") or work_dir / "data") + qualification_dir = Path(__file__).resolve().parents[1] / "data" / "qualification" + previous_evidence = _previous_gate_evidence(ledger_root, previous_certificate) + critical_spec = json.loads( + (qualification_dir / config["budget_inputs"]["critical"]).read_text() + ) + token = os.environ.get("HUGGING_FACE_TOKEN") or os.environ.get("HF_TOKEN") + state: dict[str, Any] = {} + + def dataset() -> Path: + if "dataset" not in state: + source = materialize_dataset("uk", data_dir=data_dir) + state["dataset"] = Path(source.path) + path: Path = state["dataset"] + return path + + def gate_wheels() -> GateOutcome: + paths = {} + for package, wheel in ( + (pair.model, resolved.model_wheel), + (pair.core, resolved.core_wheel), + ): + paths[package.name] = _download( + wheel.url, work_dir / "wheels" / wheel.filename, get=requests.get + ) + return wheel_hashes( + pair, + wheel_paths=paths, + pypi_digests={ + pair.model.name: resolved.model_wheel.sha256, + pair.core.name: resolved.core_wheel.sha256, + }, + ) + + def gate_core() -> GateOutcome: + if "manifest" not in state: + return _result( + "core_compatibility", + "error", + "The release manifest was not read (data_access did not succeed).", + {}, + ) + return core_compatibility( + pair, state["manifest"], model_requires_core=resolved.model_requires_core + ) + + def gate_data_access() -> GateOutcome: + result, evidence, manifest = data_access( + pair, get=requests.get, head=requests.head, token=token + ) + if manifest is not None: + state["manifest"] = manifest + return result, evidence + + def previous_environment() -> tuple[str, Path]: + if "previous_environment" not in state: + assert previous_certificate is not None + pins = _previous_pair_pins(previous_certificate, ledger_root) + state["previous_environment"] = ( + previous_pair_python(pins, work_dir), + previous_pair_dataset(pins, work_dir), + ) + return state["previous_environment"] + + def previous_snapshot() -> Optional[dict[str, Any]]: + """The previous pair's inputs and years: from the ledger, else computed.""" + + if "previous_snapshot" in state: + cached: Optional[dict[str, Any]] = state["previous_snapshot"] + return cached + snapshot: Optional[dict[str, Any]] = None + recorded_inputs = previous_evidence.get("budget_inputs") + recorded_years = previous_evidence.get("supported_years") + if recorded_inputs and recorded_years: + snapshot = { + "missing_inputs": recorded_inputs.get("missing_inputs"), + "unusable_critical": recorded_inputs.get("unusable_critical", []), + "supported_years": recorded_years.get("supported_years"), + "source": "ledger", + } + elif previous_certificate: + previous_python, previous_dataset = previous_environment() + snapshot = run_inputs_snapshot( + previous_python, + critical=critical_spec["critical"], + out=work_dir / "previous-inputs.json", + dataset_path=previous_dataset, + ) + snapshot["source"] = ( + f"computed with policyengine {previous_certificate['bundle_version']}" + ) + state["previous_snapshot"] = snapshot + return snapshot + + def gate_years() -> GateOutcome: + previous = previous_snapshot() + previous_years = previous.get("supported_years") if previous else None + return supported_years( + pair, + dataset(), + required_years=config["required_years"], + budget_horizon_years=config["budget_horizon_years"], + previous_supported_years=previous_years, + ) + + def gate_inputs() -> GateOutcome: + from policyengine_uk.system import system + + previous = previous_snapshot() + result, evidence = budget_inputs( + pair, + dataset(), + model_input_variables=[ + name + for name, variable in system.variables.items() + if variable.is_input_variable() + ], + critical_inputs=critical_spec["critical"], + accepted_gaps=critical_spec.get("accepted_gaps", {}), + previous_missing_inputs=previous.get("missing_inputs") + if previous + else None, + previous_unusable_critical=( + [item[0] for item in previous.get("unusable_critical", [])] + if previous + else None + ), + ) + evidence["previous_source"] = previous.get("source") if previous else None + return result, evidence + + def gate_contract() -> GateOutcome: + settings = config["reform_contract"] + return reform_contract( + entrypoint=settings.get("entrypoint"), + dataset_path=dataset(), + seed=int(settings.get("seed", 0)), + max_examples=int(settings.get("max_examples", 50)), + expected_versions={ + pair.model.name: pair.model.version, + pair.core.name: pair.core.version, + }, + expected_dataset_sha256=pair.data.dataset_sha256, + pending_decisions=settings.get("pending_decisions", ()), + ) + + def metrics_tool() -> Path: + if "metrics_tool" not in state: + state["metrics_tool"] = vendor_metrics_tool( + config["metrics"]["tool"], work_dir, get=requests.get + ) + tool_root: Path = state["metrics_tool"] + return tool_root + + def standard_reforms() -> list[dict[str, Any]]: + if "standard_reforms" not in state: + state["standard_reforms"] = load_standard_reforms( + metrics_tool(), + config["reform_responsiveness"]["standard_reforms_file"], + ) + reforms: list[dict[str, Any]] = state["standard_reforms"] + return reforms + + def candidate_metrics() -> dict[str, Any]: + """The candidate's report, computed once for both metric gates.""" + + import sys + + if "candidate_metrics" not in state: + state["candidate_metrics"] = run_metrics_report( + sys.executable, + metrics_tool(), + years=config["metrics"]["years"], + out=work_dir / "metrics-candidate.json", + dataset_path=dataset(), + label="candidate", + ) + state.setdefault("metrics_reports", {})["candidate"] = state[ + "candidate_metrics" + ] + report: dict[str, Any] = state["candidate_metrics"] + return report + + def gate_responsiveness() -> GateOutcome: + settings = config.get("reform_responsiveness") + if not settings or not config["metrics"].get("tool"): + return reform_responsiveness( + report=None, + standard_reforms=[], + years=[], + unavailable_reason=( + "No reform responsiveness settings or metrics tool is configured." + ), + ) + result, evidence = reform_responsiveness( + report=candidate_metrics(), + standard_reforms=standard_reforms(), + years=config["metrics"]["years"], + ) + evidence["tool"] = dict(config["metrics"]["tool"]) + return result, evidence + + def gate_metrics() -> GateOutcome: + import sys + + settings = config["metrics"] + tool = settings.get("tool") + if not tool: + return metrics_review( + candidate_report=None, + previous_report=None, + bounds={}, + unavailable_reason="No metrics tool commit is configured (metrics.tool).", + ) + if not previous_certificate: + return metrics_review( + candidate_report=None, + previous_report=None, + bounds={}, + unavailable_reason="No previous certificate to compare against.", + ) + bounds = json.loads((qualification_dir / settings["bounds"]).read_text()) + previous_python, previous_dataset = previous_environment() + vendor_root = metrics_tool() + years = settings["years"] + candidate_report = candidate_metrics() + previous_report = run_metrics_report( + previous_python, + vendor_root, + years=years, + out=work_dir / "metrics-previous.json", + dataset_path=previous_dataset, + label="previous", + ) + cross_report = None + if previous_certificate.get("dataset_sha256") != pair.data.dataset_sha256: + cross_report = run_metrics_report( + sys.executable, + vendor_root, + years=years, + out=work_dir / "metrics-candidate-model-previous-data.json", + dataset_path=Path(previous_report["dataset_path"]), + label="candidate model, previous data", + ) + state.setdefault("metrics_reports", {}).update( + previous=previous_report, + candidate_model_previous_data=cross_report, + ) + result, evidence = metrics_review( + candidate_report=candidate_report, + previous_report=previous_report, + bounds=bounds, + cross_report=cross_report, + ) + evidence["tool"] = dict(tool) + evidence["partition_sources"] = { + "candidate": candidate_report.get("partition_source"), + "previous": previous_report.get("partition_source"), + } + return result, evidence + + gates = [ + ("installed_versions", lambda: installed_versions(pair, harness)), + ("wheel_hashes", gate_wheels), + ("data_access", gate_data_access), + ("core_compatibility", gate_core), + ("dataset_bytes", lambda: dataset_bytes(pair, dataset())), + ("supported_years", gate_years), + ("budget_inputs", gate_inputs), + ("reform_contract", gate_contract), + ("metrics_review", gate_metrics), + ("reform_responsiveness", gate_responsiveness), + ] + results, evidence = run_gates(gates) + for name, report in (state.get("metrics_reports") or {}).items(): + if report is not None: + evidence[f"metrics_report_{name}"] = publishable_metrics_report(report) + return results, evidence + + +def version_sort_key(version: str) -> Version: + return Version(version) diff --git a/src/policyengine/provenance/uk_qualification.py b/src/policyengine/provenance/uk_qualification.py new file mode 100644 index 00000000..6d711cfc --- /dev/null +++ b/src/policyengine/provenance/uk_qualification.py @@ -0,0 +1,1481 @@ +"""Orchestration for qualifying a UK model/data candidate. + +``scripts/check_uk_candidate.py`` is the command-line front end and +``.github/workflows/certify-uk-release.yaml`` runs it. The steps are: + +``resolve`` + Find the candidate pair: the newest (or requested) policyengine-uk wheel + from PyPI, the core it needs, and the newest (or requested) uk-data + release at the data repository's current manifest revision. Decide + whether to qualify it, or whether it is already certified, stale, or a + duplicate of an attempt in the ledger. +``prepare`` + On a checkout of ``main``, certify the pair into the bundle manifest + (exact ``==`` claim backed by the pair id when no publisher claim covers + it), regenerate pins, ``uv.lock``, TROs, the certified-version test + constants and household snapshots together, and commit that as the + candidate commit on ``candidate/uk/-``. +``gates`` + Run the mandatory local gates on the candidate commit + (:mod:`policyengine.provenance.uk_candidate`). +``request-replay`` / ``poll-replay`` + Send the scorecard an immutable replay request naming the candidate + commit, and read back its terminal status and receipt. +``finalize`` + Record the qualification receipt in the ledger. +``promote`` + Pick the newest promotable pair that advances ``main`` and mark its + candidate PR ready, closing superseded candidates. + +Network, git and GitHub access are injected so every decision can be tested +offline. +""" + +from __future__ import annotations + +import hashlib +import json +import os +import re +import subprocess +from collections.abc import Callable, Iterable, Mapping, Sequence +from dataclasses import dataclass, field +from datetime import UTC, datetime, timedelta +from pathlib import Path +from typing import Any, Literal, Optional + +from packaging.requirements import Requirement +from packaging.specifiers import SpecifierSet +from packaging.version import InvalidVersion, Version + +from policyengine.provenance.manifest import DataReleaseManifest +from policyengine.provenance.qualification import ( + REQUIRED_GATES, + CandidatePair, + GateResult, + IssuedBy, + PinnedData, + PinnedWheel, + PreviousCertificate, + QualificationHarness, + QualificationReceipt, + ReplayQualificationRequest, + ReplayReceipt, + ReplayReference, + ReplayStatus, + ReviewAcceptance, + dominates, + evaluate_replay_receipt, + qualification_verdict, + replay_gate_while_pending, + select_promotion_target, +) +from policyengine.provenance.qualification_ledger import ( + attempt_directory, + pair_directory, + read_json, + write_json, +) +from policyengine.provenance.receipt import certificate_sha256 + +UK_MODEL_PACKAGE = "policyengine-uk" +CORE_PACKAGE = "policyengine-core" +UK_DATA_REPO_ID = "policyengine/policyengine-uk-data-private" +UK_DATA_REPO_TYPE: Literal["model"] = "model" +TOKEN_VARIABLES = ("HF_TOKEN", "HUGGING_FACE_TOKEN", "HUGGINGFACE_HUB_TOKEN") +#: The scorecard's certification recipe (policyengine-scorecard +#: pipeline/uk_replay_variants.py, isolate_certification_environment), which +#: expected_release_bundle must reproduce exactly. policyengine.py 6.x fetches +#: the data release manifest with requests, which ignores HF_HUB_OFFLINE; with +#: these proxies the fetch fails at once for a private or public data repo and +#: certify_data_release_compatibility falls back to the packaged certification. +PROXY_EXCEPTIONS = ("NO_PROXY", "no_proxy") +OFFLINE_VARIABLES = { + "HF_HUB_OFFLINE": "1", + "HF_DATASETS_OFFLINE": "1", + "TRANSFORMERS_OFFLINE": "1", + "HF_HUB_DISABLE_TELEMETRY": "1", + "HF_HUB_DISABLE_IMPLICIT_TOKEN": "1", +} +NO_NETWORK = { + name: "http://127.0.0.1:9" + for name in ( + "HTTP_PROXY", + "HTTPS_PROXY", + "ALL_PROXY", + "http_proxy", + "https_proxy", + "all_proxy", + ) +} + + +def certification_environment(environ: Mapping[str, str]) -> dict[str, str]: + """``environ`` as the scorecard isolates it before reading release_bundle.""" + + env = { + key: value + for key, value in environ.items() + if key not in TOKEN_VARIABLES and key not in PROXY_EXCEPTIONS + } + env.update(OFFLINE_VARIABLES) + env.update(NO_NETWORK) + return env + + +REPO_ROOT = Path(__file__).resolve().parents[3] + +Action = Literal["qualify", "already_certified", "stale", "duplicate"] + + +# --- Resolution ------------------------------------------------------------ + + +@dataclass(frozen=True) +class WheelFile: + filename: str + url: str + sha256: str + + +@dataclass(frozen=True) +class ResolvedCandidate: + """A candidate pair plus what is needed to fetch and certify it.""" + + pair: CandidatePair + model_wheel: WheelFile + core_wheel: WheelFile + manifest_uri: str + model_requires_core: str + + +def _stable_versions( + releases: Mapping[str, Sequence[Mapping[str, Any]]], +) -> list[Version]: + versions = [] + for text, files in releases.items(): + try: + version = Version(text) + except InvalidVersion: + continue + if version.is_prerelease or version.is_devrelease: + continue + if not files or all(item.get("yanked") for item in files): + continue + versions.append(version) + return sorted(versions) + + +def _wheel(files: Sequence[Mapping[str, Any]], name: str, version: str) -> WheelFile: + wheels = [ + item + for item in files + if item.get("packagetype") == "bdist_wheel" and not item.get("yanked") + ] + if len(wheels) != 1: + raise ValueError( + f"{name} {version} must publish exactly one wheel; found {len(wheels)}." + ) + entry = wheels[0] + return WheelFile( + filename=entry["filename"], url=entry["url"], sha256=entry["digests"]["sha256"] + ) + + +def core_requirement(requires_dist: Iterable[str]) -> str: + """The model's ``policyengine-core`` specifier (unconditional requirements).""" + + for line in requires_dist or (): + requirement = Requirement(line) + if requirement.name.lower().replace("_", "-") != CORE_PACKAGE: + continue + if requirement.marker is not None and not requirement.marker.evaluate(): + continue + return str(requirement.specifier) + raise ValueError("policyengine-uk declares no policyengine-core requirement.") + + +def choose_core_version( + current_core: str, + specifier: str, + available: Sequence[Version], +) -> str: + """Keep the certified core if the model accepts it; else the newest accepted.""" + + spec = SpecifierSet(specifier) + if Version(current_core) in spec: + return current_core + accepted = [version for version in available if version in spec] + if not accepted: + raise ValueError( + f"No published policyengine-core release satisfies {specifier}." + ) + return str(max(accepted)) + + +def latest_data_release(tags: Iterable[str]) -> str: + """The newest release tag that is a plain version (``1.58.0``).""" + + versions = [] + for tag in tags: + if not re.fullmatch(r"\d+(\.\d+)*", tag): + continue + versions.append(Version(tag)) + if not versions: + raise ValueError("The data repository has no version tags.") + return str(max(versions)) + + +def resolve_candidate( + *, + current_core: str, + pypi_project: Callable[[str], Mapping[str, Any]], + hf_revision_sha: Callable[[str, str], str], + hf_tags: Callable[[str], Sequence[str]], + hf_file: Callable[[str, str, str], bytes], + model_version: Optional[str] = None, + data_version: Optional[str] = None, +) -> ResolvedCandidate: + """Resolve the candidate pair from PyPI and Hugging Face.""" + + model_project = pypi_project(UK_MODEL_PACKAGE) + releases = model_project["releases"] + model_version = model_version or str(_stable_versions(releases)[-1]) + if model_version not in releases: + raise ValueError(f"{UK_MODEL_PACKAGE} {model_version} is not on PyPI.") + model_wheel = _wheel(releases[model_version], UK_MODEL_PACKAGE, model_version) + model_release = pypi_project(f"{UK_MODEL_PACKAGE}/{model_version}") + requires_core = core_requirement(model_release["info"].get("requires_dist") or []) + + core_project = pypi_project(CORE_PACKAGE) + core_version = choose_core_version( + current_core, requires_core, _stable_versions(core_project["releases"]) + ) + core_wheel = _wheel( + core_project["releases"][core_version], CORE_PACKAGE, core_version + ) + + data_version = data_version or latest_data_release(hf_tags(UK_DATA_REPO_ID)) + manifest_revision = hf_revision_sha(UK_DATA_REPO_ID, "main") + manifest_path = f"releases/{data_version}/release_manifest.json" + manifest_bytes = hf_file(UK_DATA_REPO_ID, manifest_revision, manifest_path) + manifest = DataReleaseManifest.model_validate_json(manifest_bytes) + if manifest.data_package.version != data_version: + raise ValueError( + f"{manifest_path} describes {manifest.data_package.version}, not {data_version}." + ) + dataset = manifest.default_datasets.get("national") + if dataset is None or dataset not in manifest.artifacts: + raise ValueError( + f"uk-data {data_version} declares no national default dataset." + ) + artifact = manifest.artifacts[dataset] + if not artifact.sha256 or not artifact.size_bytes: + raise ValueError(f"uk-data {data_version} {dataset} has no sha256 or size.") + build_id = ( + manifest.build.build_id if manifest.build and manifest.build.build_id else None + ) + if build_id is None: + raise ValueError(f"uk-data {data_version} records no build id.") + + pair = CandidatePair( + country_id="uk", + model=PinnedWheel( + name=UK_MODEL_PACKAGE, + version=model_version, + wheel_sha256=model_wheel.sha256, + ), + core=PinnedWheel( + name=CORE_PACKAGE, version=core_version, wheel_sha256=core_wheel.sha256 + ), + data=PinnedData( + data_package=manifest.data_package.name, + release_version=data_version, + build_id=build_id, + repo_id=UK_DATA_REPO_ID, + repo_type=UK_DATA_REPO_TYPE, + release_manifest_path=manifest_path, + release_manifest_revision=manifest_revision, + release_manifest_sha256=hashlib.sha256(manifest_bytes).hexdigest(), + dataset=dataset, + dataset_path=artifact.path, + dataset_revision=artifact.revision, + dataset_sha256=artifact.sha256, + size_bytes=artifact.size_bytes, + ), + ) + return ResolvedCandidate( + pair=pair, + model_wheel=model_wheel, + core_wheel=core_wheel, + manifest_uri=( + f"hf://{UK_DATA_REPO_TYPE}/{UK_DATA_REPO_ID}@{manifest_revision}/{manifest_path}" + ), + model_requires_core=requires_core, + ) + + +# --- Deciding what to do ------------------------------------------------------ + + +def certified_order_key(bundle: Mapping[str, Any]) -> tuple[str, str, str]: + """(model version, data release, core version) certified on a branch.""" + + record = bundle["data_releases"]["uk"] + return ( + record["model_package"]["version"], + record["data_package"]["version"], + bundle["packages"][CORE_PACKAGE]["version"], + ) + + +def previous_certificate(bundle: Mapping[str, Any]) -> PreviousCertificate: + record = bundle["data_releases"]["uk"] + reference = record["datasets"][record["default_dataset"]] + data_package = record["data_package"] + return PreviousCertificate( + bundle_version=bundle["policyengine_version"], + model_version=record["model_package"]["version"], + core_version=bundle["packages"][CORE_PACKAGE]["version"], + data_release_version=record["data_package"]["version"], + dataset_sha256=record["certified_data_artifact"]["sha256"], + certificate_sha256=certificate_sha256(record), + qualification_pair_id=(record.get("certification") or {}).get( + "qualification_pair_id" + ), + model_wheel_sha256=record["model_package"].get("sha256"), + core_wheel_sha256=bundle["packages"][CORE_PACKAGE].get("sha256"), + dataset_reference={ + **reference, + "repo_id": reference.get("repo_id") or data_package["repo_id"], + "repo_type": reference.get("repo_type") + or data_package.get("repo_type", "model"), + }, + dataset_name=record["default_dataset"], + release_manifest_reference={ + "repo_id": data_package["repo_id"], + "repo_type": data_package.get("repo_type", "model"), + "path": data_package["release_manifest_path"], + "revision": data_package["release_manifest_revision"], + }, + ) + + +def candidate_action( + pair: CandidatePair, + bundle: Mapping[str, Any], + ledger_root: Optional[Path], + *, + base_commit: str, + force: bool = False, + now: Optional[str] = None, + owner_is_failed: Optional[Callable[[str], bool]] = None, +) -> tuple[Action, str]: + """Whether to qualify ``pair`` now, and why.""" + + current = certified_order_key(bundle) + record = bundle["data_releases"]["uk"] + if pair.order_key == current and ( + record["certified_data_artifact"]["sha256"] == pair.data.dataset_sha256 + ): + return "already_certified", f"{current} is already certified on main" + if dominates(current, pair.order_key): + return "stale", f"main certifies {current}, newer than {pair.order_key}" + if force or ledger_root is None: + return "qualify", "forced" if force else "no ledger" + pair_dir = pair_directory(ledger_root, "uk", pair.pair_id) + latest = pair_dir / "qualification_receipt.json" + receipt = ( + QualificationReceipt.model_validate(read_json(latest)) + if latest.exists() + else None + ) + attempt = _latest_base_attempt(pair, ledger_root, base_commit) + recovery = _latest_staged_attempt(pair, ledger_root) + if ( + recovery is not None + and attempt_is_recoverable(recovery, now=now, owner_is_failed=owner_is_failed) + and ( + receipt is None + or receipt.verdict == "incomplete" + or attempt_supersedes_receipt(recovery, receipt) + ) + ): + return "qualify", "recoverable abandoned staged attempt" + if receipt is not None and receipt.verdict != "incomplete": + return "duplicate", f"pair already {receipt.verdict} in the ledger" + if attempt is not None: + return "duplicate", f"an attempt from base {base_commit[:12]} exists" + return "qualify", "new pair, or an incomplete pair on a new base commit" + + +# --- Preparing the candidate commit ----------------------------------------- + +#: Test files that pin the certified UK pair literally. Promotion regenerates +#: them with the rest of the certification so the suite tests the new pair. +CERTIFIED_CONSTANT_FILES = ( + "tests/test_models.py", + "tests/test_release_manifests.py", + "tests/test_uk_regions.py", +) + + +def certified_constant_replacements( + old_record: Mapping[str, Any], new_record: Mapping[str, Any] +) -> list[tuple[str, str]]: + """Exact quoted-literal replacements from the old to the new certificate.""" + + def values(record: Mapping[str, Any]) -> dict[str, Optional[str]]: + certification = record.get("certification") or {} + data = record.get("data_package") or {} + return { + "model": (record.get("model_package") or {}).get("version"), + "built_with": certification.get("built_with_model_version"), + "data": data.get("version"), + "build_id": certification.get("data_build_id"), + "revision": data.get("release_manifest_revision"), + } + + old, new = values(old_record), values(new_record) + pairs: list[tuple[str, str]] = [] + for key in ("build_id", "revision", "model", "built_with", "data"): + before, after = old[key], new[key] + if before and after and before != after: + pairs.append((before, after)) + return pairs + + +def rewrite_certified_constants( + repo_root: Path, replacements: Sequence[tuple[str, str]] +) -> list[str]: + """Rewrite quoted occurrences of old certified values in the constant files. + + Only string-literal occurrences change: ``"2.125.1"``, ``"@1.58.0"``, + ``"releases/1.58.0/release_manifest.json"`` and URIs ending ``@1.58.0``. + Returns the files changed. + """ + + changed = [] + for relative in CERTIFIED_CONSTANT_FILES: + path = repo_root / relative + text = path.read_text() + updated = text + for old, new in replacements: + pattern = re.compile( + r'(?P
["@/-])' + re.escape(old) + r'(?P["/])'
+            )
+            updated = pattern.sub(
+                lambda m: m.group("pre") + new + m.group("post"), updated
+            )
+        if updated != text:
+            path.write_text(updated)
+            changed.append(relative)
+    return changed
+
+
+def candidate_branch(pair: CandidatePair) -> str:
+    """The pair's own branch.
+
+    The pair id is part of the name: two pairs that share model and data
+    versions (another core, another wheel, re-published bytes) must never
+    share a branch, or one pair's qualification could promote the other's
+    commit.
+    """
+
+    return (
+        f"candidate/uk/{pair.model.version}-{pair.data.release_version}"
+        f"-{pair.pair_id[:12]}"
+    )
+
+
+def attempt_key(
+    pair: CandidatePair,
+    ledger_root: Optional[Path],
+    *,
+    base_commit: str,
+    force: bool = False,
+    now: Optional[str] = None,
+    owner_is_failed: Optional[Callable[[str], bool]] = None,
+) -> str:
+    """The ledger key of the attempt a qualification run would stage.
+
+    Concurrent runs for the same pair and base deduplicate. Forced runs and
+    recovery of abandoned stages use the next free key, preserving earlier
+    evidence. Recovery follows the newest stage across bases so an unrelated
+    main commit cannot strand an authorized forced replacement.
+    """
+
+    key = base_commit[:12]
+    if ledger_root is None:
+        return key
+    attempt = _latest_staged_attempt(pair, ledger_root)
+    recovering = attempt is not None and attempt_is_recoverable(
+        attempt, now=now, owner_is_failed=owner_is_failed
+    )
+    if not force and not recovering:
+        return key
+    if not attempt_directory(ledger_root, "uk", pair.pair_id, key).exists():
+        return key
+    repeat = 2
+    while attempt_directory(
+        ledger_root, "uk", pair.pair_id, f"{key}-{repeat}"
+    ).exists():
+        repeat += 1
+    return f"{key}-{repeat}"
+
+
+def _latest_staged_attempt(pair: CandidatePair, ledger_root: Path) -> Optional[Path]:
+    """The pair's newest durable stage, independent of its main base commit.
+
+    A newer stage supersedes older recovery work across bases. Inspecting just
+    the current base could overlook abandoned forced work or retry an older
+    stage while its replacement is live.
+    """
+
+    parent = pair_directory(ledger_root, "uk", pair.pair_id) / "attempts"
+    stage = max(
+        parent.glob("*/staging.json"),
+        key=lambda path: int(read_json(path)["sequence"]),
+        default=None,
+    )
+    return stage.parent if stage is not None else None
+
+
+def _latest_base_attempt(
+    pair: CandidatePair, ledger_root: Path, base_commit: str
+) -> Optional[Path]:
+    base = base_commit[:12]
+    parent = pair_directory(ledger_root, "uk", pair.pair_id) / "attempts"
+    attempts = [
+        path
+        for path in parent.glob(f"{base}*")
+        if path.is_dir() and re.fullmatch(re.escape(base) + r"(?:-\d+)?", path.name)
+    ]
+
+    def order(path: Path) -> tuple[int, int]:
+        stage = path / "staging.json"
+        sequence = int(read_json(stage)["sequence"]) if stage.exists() else 0
+        repeat = int(path.name[len(base) + 1 :]) if path.name != base else 1
+        return sequence, repeat
+
+    return max(attempts, key=order, default=None)
+
+
+def changelog_fragment(pair: CandidatePair, basis_note: str) -> str:
+    return (
+        f"Certify uk-data {pair.data.release_version} (`{pair.data.dataset}`) with "
+        f"policyengine-uk {pair.model.version} and policyengine-core "
+        f"{pair.core.version}, qualified automatically as pair "
+        f"`{pair.pair_id[:12]}`. {basis_note}\n"
+    )
+
+
+Runner = Callable[..., subprocess.CompletedProcess]
+
+
+def _run(
+    command: Sequence[str], *, cwd: Path, env: Optional[Mapping[str, str]] = None
+) -> subprocess.CompletedProcess:
+    return subprocess.run(
+        list(command), cwd=cwd, env=dict(env) if env is not None else None, check=True
+    )
+
+
+def prepare_candidate_commit(
+    repo_root: Path,
+    resolved: ResolvedCandidate,
+    *,
+    python: str,
+    run: Runner = _run,
+    refresh_snapshots: bool = True,
+) -> dict[str, Any]:
+    """Certify the pair into the working tree and regenerate everything with it.
+
+    The working tree must be a clean checkout of the base commit on the
+    candidate branch. Returns what changed; the caller commits it.
+    """
+
+    pair = resolved.pair
+    bundle_path = (
+        repo_root / "src" / "policyengine" / "data" / "bundle" / "manifest.json"
+    )
+    old_record = json.loads(bundle_path.read_text())["data_releases"]["uk"]
+    run(
+        [
+            python,
+            "scripts/bundle.py",
+            "certify-data",
+            "--country",
+            "uk",
+            "--data-producer",
+            "populace",
+            "--manifest-uri",
+            resolved.manifest_uri,
+            "--model-version",
+            pair.model.version,
+            "--core-version",
+            pair.core.version,
+            "--qualified-pair-id",
+            pair.pair_id,
+            "--no-changelog",
+        ],
+        cwd=repo_root,
+    )
+    new_bundle = json.loads(bundle_path.read_text())
+    new_record = new_bundle["data_releases"]["uk"]
+    if new_record["model_package"].get("sha256") != pair.model.wheel_sha256:
+        raise ValueError("The certified model wheel hash differs from the candidate's.")
+    if new_record["certified_data_artifact"].get("sha256") != pair.data.dataset_sha256:
+        raise ValueError("The certified dataset hash differs from the candidate's.")
+    run(
+        [
+            "uv",
+            "lock",
+            "--no-config",
+            "--no-sources",
+            "--default-index",
+            "https://pypi.org/simple",
+            "--python",
+            python,
+        ],
+        cwd=repo_root,
+    )
+    run([python, "scripts/release_lock.py"], cwd=repo_root)
+    # Prepare the candidate's replacement sidecars before strict verification:
+    # the committed sidecars still authenticate the previous data release.
+    run(
+        [python, "scripts/bundle.py", "generate", "--include-tros"],
+        cwd=repo_root,
+    )
+    constants = rewrite_certified_constants(
+        repo_root, certified_constant_replacements(old_record, new_record)
+    )
+    basis = new_record["certification"]["compatibility_basis"]
+    basis_note = {
+        "built_with_model_package": "The data was built with this model version.",
+        "legacy_compatible_model_package": "The data publisher's compatibility claim covers this model version.",
+        "qualified_exact_pair": "No publisher claim covers this model version, so the certificate is an exact claim backed by the qualification receipt.",
+    }.get(basis, f"Compatibility basis: {basis}.")
+    fragment = (
+        repo_root
+        / "changelog.d"
+        / (f"certify-uk-{pair.model.version}-{pair.data.release_version}.changed.md")
+    )
+    fragment.write_text(changelog_fragment(pair, basis_note))
+    if refresh_snapshots:
+        run(
+            [
+                python,
+                "-m",
+                "pytest",
+                "tests/test_household_calculator_snapshot.py",
+                "-q",
+            ],
+            cwd=repo_root,
+            env={**os.environ, "PE_UPDATE_SNAPSHOTS": "1"},
+        )
+    run(
+        [python, "scripts/bundle.py", "check", "--include-tros", "--strict-tros"],
+        cwd=repo_root,
+    )
+    return {
+        "compatibility_basis": basis,
+        "constant_files": constants,
+        "changelog_fragment": str(fragment.relative_to(repo_root)),
+    }
+
+
+def packaged_manifest_sha256(repo_root: Path) -> str:
+    path = repo_root / "src" / "policyengine" / "data" / "bundle" / "manifest.json"
+    return hashlib.sha256(path.read_bytes()).hexdigest()
+
+
+# --- Replay request -------------------------------------------------------
+
+
+def expected_release_bundle(python: str, *, cwd: Path) -> dict[str, Optional[str]]:
+    """``dict(pe.uk.uk_latest.release_bundle)`` under packaged certification.
+
+    Runs in a subprocess in the scorecard's certification environment (no
+    tokens, no network for the release-manifest request), so the bundled
+    certification is what is reported for a private or public data repo.
+    """
+
+    env = certification_environment(os.environ)
+    output = subprocess.run(
+        [
+            python,
+            "-c",
+            # The CLI disables automatic country imports. Load the UK API
+            # explicitly so the flag cannot leave pe.uk unset and unrelated
+            # country models need not be initialized for this UK request.
+            "import json; from policyengine.tax_benefit_models.uk import uk_latest; "
+            "print(json.dumps(dict(uk_latest.release_bundle), sort_keys=True))",
+        ],
+        cwd=cwd,
+        env=env,
+        check=True,
+        capture_output=True,
+        text=True,
+    ).stdout
+    bundle: dict[str, Optional[str]] = json.loads(output.strip().splitlines()[-1])
+    return bundle
+
+
+def build_replay_request(
+    pair: CandidatePair,
+    harness: QualificationHarness,
+    *,
+    release_bundle: Mapping[str, Optional[str]],
+    issued_by: IssuedBy,
+    attempt: int = 1,
+    compare_to: str = "latest_certified",
+) -> ReplayQualificationRequest:
+    """The immutable pre-promotion request for the scorecard."""
+
+    return ReplayQualificationRequest.model_validate(
+        {
+            "schema": "policyengine.uk-replay-qualification-request/v1",
+            "kind": "candidate",
+            "attempt": attempt,
+            "candidate": {
+                "policyengine": {
+                    "version": harness.policyengine_version,
+                    "source": {
+                        "git": {
+                            "repository": "PolicyEngine/policyengine.py",
+                            "commit": harness.git_commit,
+                        }
+                    },
+                    "packaged_bundle_manifest_sha256": harness.packaged_bundle_manifest_sha256,
+                },
+                "policyengine_uk": {
+                    "version": pair.model.version,
+                    "wheel_sha256": pair.model.wheel_sha256,
+                },
+                "policyengine_core": {
+                    "version": pair.core.version,
+                    "wheel_sha256": pair.core.wheel_sha256,
+                },
+                "data": {
+                    "build_id": pair.data.build_id,
+                    "dataset": pair.data.dataset,
+                    "repo_id": pair.data.repo_id,
+                    "repo_type": pair.data.repo_type,
+                    "revision": pair.data.dataset_revision,
+                    "path": pair.data.dataset_path,
+                    "sha256": pair.data.dataset_sha256,
+                    "size_bytes": pair.data.size_bytes,
+                    "release_manifest_sha256": pair.data.release_manifest_sha256,
+                },
+                "expected_release_bundle": dict(release_bundle),
+            },
+            "compare_to": compare_to,
+            "requested_by": {
+                "repository": issued_by.repository,
+                "workflow_run_id": issued_by.workflow_run_id,
+                "run_attempt": issued_by.run_attempt,
+                "commit": harness.git_commit,
+                "pair_id": pair.pair_id,
+            },
+        }
+    )
+
+
+def replay_dispatch_payload(
+    request: ReplayQualificationRequest, *, event_type: str
+) -> dict[str, Any]:
+    return {"event_type": event_type, "client_payload": {"request": request.document()}}
+
+
+def replay_state_paths(request_id: str) -> tuple[str, str]:
+    base = f"requests/{request_id}"
+    return f"{base}/STATUS.json", f"{base}/REPLAY_RECEIPT.json"
+
+
+def read_replay(
+    request: ReplayQualificationRequest,
+    *,
+    fetch: Callable[[str, str], Optional[bytes]],
+    state_ref: str,
+) -> tuple[GateResult, Optional[ReplayStatus], Optional[ReplayReceipt]]:
+    """Read the scorecard's status and, when terminal, its receipt.
+
+    ``fetch(ref, path)`` returns file bytes from the scorecard repository at
+    a ref or commit, or ``None`` if absent.
+    """
+
+    status_path, receipt_path = replay_state_paths(request.request_id)
+    raw_status = fetch(state_ref, status_path)
+    if raw_status is None:
+        return replay_gate_while_pending(None), None, None
+    status = ReplayStatus.model_validate_json(raw_status)
+    if not status.terminal:
+        return replay_gate_while_pending(status), status, None
+    assert status.receipt_commit is not None
+    raw_receipt = fetch(status.receipt_commit, receipt_path)
+    if raw_receipt is None:
+        return (
+            GateResult(
+                gate_id="replay",
+                status="fail",
+                summary=(
+                    f"Status names receipt commit {status.receipt_commit}, which has "
+                    "no REPLAY_RECEIPT.json."
+                ),
+            ),
+            status,
+            None,
+        )
+    receipt = ReplayReceipt.from_document(json.loads(raw_receipt))
+    return evaluate_replay_receipt(request, status, receipt), status, receipt
+
+
+# --- Finalizing ---------------------------------------------------------------
+
+
+def assemble_receipt(
+    pair: CandidatePair,
+    harness: QualificationHarness,
+    gates: Sequence[GateResult],
+    *,
+    issued_by: IssuedBy,
+    completed_at: str,
+    previous: Optional[PreviousCertificate],
+    replay: Optional[ReplayReference],
+) -> QualificationReceipt:
+    """A receipt whose verdict follows from the gates; missing gates count as missing."""
+
+    ordered = sorted(
+        gates,
+        key=lambda gate: (
+            REQUIRED_GATES.index(gate.gate_id)
+            if gate.gate_id in REQUIRED_GATES
+            else len(REQUIRED_GATES),
+            gate.gate_id,
+        ),
+    )
+    return QualificationReceipt(
+        schema_version=1,
+        receipt_type="policyengine.candidate_qualification_receipt",
+        pair_id=pair.pair_id,
+        pair=pair,
+        harness=harness,
+        previous_certificate=previous,
+        replay=replay,
+        gates=list(ordered),
+        verdict=qualification_verdict(ordered),
+        issued_by=issued_by,
+        completed_at=completed_at,
+    )
+
+
+def accept_review(
+    receipt: QualificationReceipt,
+    *,
+    reviewer: str,
+    accepted_receipt_sha256: str,
+    explanation: str,
+    accepted_at: str,
+) -> QualificationReceipt:
+    """Attach a reviewer's acceptance of exactly this needs-review receipt."""
+
+    if receipt.verdict != "needs_review":
+        raise ValueError(
+            f"Only needs-review receipts can be accepted, not {receipt.verdict}."
+        )
+    if accepted_receipt_sha256 != receipt.unreviewed_sha256:
+        raise ValueError(
+            "The acceptance names different receipt bytes "
+            f"({accepted_receipt_sha256}, current {receipt.unreviewed_sha256})."
+        )
+    return receipt.model_copy(
+        update={
+            "review": ReviewAcceptance(
+                reviewer=reviewer,
+                accepted_at=accepted_at,
+                accepted_receipt_sha256=accepted_receipt_sha256,
+                explanation=explanation,
+            )
+        }
+    )
+
+
+# --- Attempt directories -----------------------------------------------------
+#
+# Every step of an attempt reads and writes one directory inside the ledger
+# checkout (``//attempts//``), so a
+# scheduled catch-up can finish an attempt whose replay outlasted its job with
+# exactly the code path the job would have run.
+
+
+def stage_attempt(
+    attempt_dir: Path,
+    candidate_payload: Mapping[str, Any],
+    harness: QualificationHarness,
+    *,
+    owner_run_id: Optional[str] = None,
+    staged_at: Optional[str] = None,
+) -> None:
+    payloads = {
+        attempt_dir / "candidate.json": dict(candidate_payload),
+        attempt_dir / "harness.json": harness.canonical_payload(),
+    }
+    for path, payload in payloads.items():
+        if path.exists() and read_json(path) != payload:
+            raise ValueError(f"A staged attempt's {path.name} is immutable.")
+    staging_path = attempt_dir / "staging.json"
+    if not staging_path.exists():
+        # Persist staging order in the ledger, never infer it from finalization
+        # time, commit hashes or filesystem mtimes. Legacy attempts precede
+        # newly staged attempts and keep their existing receipt bytes.
+        prior_orders = [
+            int(read_json(path)["sequence"])
+            for path in attempt_dir.parent.glob("*/staging.json")
+        ]
+        staged = _attempt_time(staged_at)
+        stage = {
+            "sequence": max(prior_orders, default=0) + 1,
+            "owner_run_id": owner_run_id,
+            "staged_at": staged.isoformat().replace("+00:00", "Z"),
+            "expires_at": (staged + timedelta(hours=4))
+            .isoformat()
+            .replace("+00:00", "Z"),
+        }
+        supersedes = candidate_payload.get("supersedes_receipt_sha256")
+        if supersedes:
+            published = attempt_dir.parent.parent / "qualification_receipt.json"
+            if (
+                not published.exists()
+                or QualificationReceipt.model_validate(
+                    read_json(published)
+                ).unreviewed_sha256
+                != supersedes
+            ):
+                raise ValueError(
+                    "Recovery must bind the exact receipt the forced attempt supersedes"
+                )
+            stage["supersedes_receipt_sha256"] = supersedes
+        write_json(staging_path, stage)
+    for path, payload in payloads.items():
+        if not path.exists():
+            write_json(path, payload)
+
+
+def write_gate_results(
+    attempt_dir: Path, results: Sequence[GateResult], evidence: Mapping[str, Any]
+) -> None:
+    for result in results:
+        write_json(
+            attempt_dir / "gates" / f"{result.gate_id}.result.json",
+            result.canonical_payload(),
+        )
+    for gate_id, item in evidence.items():
+        write_json(attempt_dir / "gates" / f"{gate_id}.json", item)
+
+
+def load_gate_results(attempt_dir: Path) -> list[GateResult]:
+    results = [
+        GateResult.model_validate(read_json(path))
+        for path in sorted((attempt_dir / "gates").glob("*.result.json"))
+    ]
+    replay = attempt_dir / "replay" / "replay.result.json"
+    if replay.exists():
+        results = [gate for gate in results if gate.gate_id != "replay"]
+        results.append(GateResult.model_validate(read_json(replay)))
+    return results
+
+
+def finalize_attempt(
+    attempt_dir: Path,
+    ledger_root: Path,
+    *,
+    issued_by: IssuedBy,
+    completed_at: str,
+) -> QualificationReceipt:
+    """Assemble and record the receipt for a staged attempt."""
+
+    receipt_path = attempt_dir / "qualification_receipt.json"
+    existing = None
+    if receipt_path.exists():
+        existing = QualificationReceipt.model_validate(read_json(receipt_path))
+        if existing.verdict != "incomplete":
+            # A terminal attempt is immutable, including its completion time.
+            # Retry publication if an earlier run stopped after saving it.
+            _publish_latest_attempt_receipt(attempt_dir, ledger_root, existing)
+            return existing
+    candidate = read_json(attempt_dir / "candidate.json")
+    pair = CandidatePair.model_validate(candidate["pair"])
+    harness = QualificationHarness.model_validate(
+        read_json(attempt_dir / "harness.json")
+    )
+    gates = load_gate_results(attempt_dir)
+    if not any(gate.gate_id == "replay" for gate in gates):
+        gates.append(replay_gate_while_pending(None))
+    previous = (
+        PreviousCertificate.model_validate(candidate["previous_certificate"])
+        if candidate.get("previous_certificate")
+        else None
+    )
+    replay_reference = None
+    request_path = attempt_dir / "replay" / "request.json"
+    if request_path.exists():
+        request = ReplayQualificationRequest.model_validate(read_json(request_path))
+        status_path = attempt_dir / "replay" / "status.json"
+        status = (
+            ReplayStatus.model_validate(read_json(status_path))
+            if status_path.exists()
+            else None
+        )
+        replay_reference = ReplayReference(
+            request_id=request.request_id,
+            receipt_sha256=status.receipt_sha256 if status else None,
+            receipt_commit=status.receipt_commit if status else None,
+        )
+    receipt = assemble_receipt(
+        pair,
+        harness,
+        gates,
+        issued_by=issued_by,
+        completed_at=completed_at,
+        previous=previous,
+        replay=replay_reference,
+    )
+    # Pending receipts may advance to terminal when replay finishes. Preserve
+    # every version under its digest; terminal attempt receipts never change.
+    if existing is not None:
+        snapshot = attempt_dir / "receipts" / f"{existing.receipt_sha256}.json"
+        if not snapshot.exists():
+            write_json(snapshot, existing.canonical_payload())
+    snapshot = attempt_dir / "receipts" / f"{receipt.receipt_sha256}.json"
+    if not snapshot.exists():
+        write_json(snapshot, receipt.canonical_payload())
+    write_json(receipt_path, receipt.canonical_payload())
+    _publish_latest_attempt_receipt(attempt_dir, ledger_root, receipt)
+    return receipt
+
+
+def _publish_latest_attempt_receipt(
+    attempt_dir: Path, ledger_root: Path, receipt: QualificationReceipt
+) -> None:
+    """Publish by staging order, repairing interrupted publication on retry."""
+
+    pair = receipt.pair
+    pair_dir = pair_directory(ledger_root, "uk", pair.pair_id)
+    write_json(pair_dir / "pair.json", pair.canonical_payload())
+    staging_path = attempt_dir / "staging.json"
+    sequence = int(read_json(staging_path)["sequence"]) if staging_path.exists() else 0
+    latest_path = pair_dir / "qualification_receipt.json"
+    latest_attempt_path = pair_dir / "latest_attempt.json"
+    latest_sequence = -1
+    latest_attempt_key = None
+    latest_pointer = (
+        read_json(latest_attempt_path) if latest_attempt_path.exists() else None
+    )
+    if latest_path.exists():
+        # Receipt identity is authoritative: a crash can leave a stale pointer.
+        # Recover staging order from immutable receipts/snapshots, including
+        # old ledgers and receipts with a receipt-bound acceptance.
+        latest = QualificationReceipt.model_validate(read_json(latest_path))
+        latest_sequence = 0
+        previous_paths = [
+            *pair_dir.glob("attempts/*/qualification_receipt.json"),
+            *pair_dir.glob("attempts/*/receipts/*.json"),
+        ]
+        for previous_path in previous_paths:
+            previous = QualificationReceipt.model_validate(read_json(previous_path))
+            if previous.unreviewed_sha256 != latest.unreviewed_sha256:
+                continue
+            previous_attempt = (
+                previous_path.parent.parent
+                if previous_path.parent.name == "receipts"
+                else previous_path.parent
+            )
+            previous_stage = previous_attempt / "staging.json"
+            previous_sequence = (
+                int(read_json(previous_stage)["sequence"])
+                if previous_stage.exists()
+                else 0
+            )
+            if previous_sequence >= latest_sequence:
+                latest_sequence = previous_sequence
+                latest_attempt_key = previous_attempt.name
+    elif latest_pointer is not None:
+        # Preserve the known order if only the publication pointer survived.
+        latest_sequence = int(latest_pointer["sequence"])
+        latest_attempt_key = latest_pointer["attempt_key"]
+    same_attempt = latest_attempt_key == attempt_dir.name
+    update_pending = (
+        same_attempt
+        and latest_path.exists()
+        and QualificationReceipt.model_validate(read_json(latest_path)).verdict
+        == "incomplete"
+    )
+    if (
+        sequence > latest_sequence
+        or update_pending
+        or (same_attempt and not latest_path.exists())
+    ):
+        write_json(latest_path, receipt.canonical_payload())
+        latest_attempt_key = attempt_dir.name
+        latest_sequence = sequence
+    expected_pointer = {"attempt_key": latest_attempt_key, "sequence": latest_sequence}
+    if latest_attempt_key is not None and latest_pointer != expected_pointer:
+        # Repair only the pointer when the published receipt already belongs
+        # to the latest attempt, preserving any receipt-bound review acceptance.
+        write_json(latest_attempt_path, expected_pointer)
+
+
+def attempt_is_waiting_for_replay(attempt_dir: Path) -> bool:
+    """A request was sent and the replay gate has not reached a terminal result."""
+
+    if not (attempt_dir / "replay" / "request.json").exists():
+        return False
+    result = attempt_dir / "replay" / "replay.result.json"
+    if not result.exists():
+        return True
+    return GateResult.model_validate(read_json(result)).status == "unavailable"
+
+
+def _attempt_time(value: Optional[str]) -> datetime:
+    instant = (
+        datetime.fromisoformat(value.replace("Z", "+00:00"))
+        if value
+        else datetime.now(UTC)
+    )
+    if instant.tzinfo is None:
+        raise ValueError("Attempt timestamps must have a timezone")
+    return instant.astimezone(UTC)
+
+
+def attempt_is_recoverable(
+    attempt_dir: Path,
+    *,
+    now: Optional[str] = None,
+    owner_is_failed: Optional[Callable[[str], bool]] = None,
+) -> bool:
+    """An abandoned stage can be retried in a new immutable attempt directory.
+
+    Sent requests retain the replay catch-up path. A live owner retains its
+    stage until expiry; unknown GitHub status never implies owner failure.
+    Older stages without ownership metadata are recoverable as unowned work.
+    """
+
+    if (attempt_dir / "replay" / "request.json").exists():
+        return False
+    receipt_path = attempt_dir / "qualification_receipt.json"
+    if (
+        receipt_path.exists()
+        and QualificationReceipt.model_validate(read_json(receipt_path)).verdict
+        != "incomplete"
+    ):
+        return False
+    stage_path = attempt_dir / "staging.json"
+    if not stage_path.exists() or not (attempt_dir / "candidate.json").exists():
+        return False
+    stage = read_json(stage_path)
+    expires = stage.get("expires_at")
+    if expires and _attempt_time(now) >= _attempt_time(expires):
+        return True
+    owner = stage.get("owner_run_id")
+    if owner and owner_is_failed is not None and owner_is_failed(str(owner)):
+        return True
+    return not owner and not expires
+
+
+def attempt_supersedes_receipt(
+    attempt_dir: Path, receipt: QualificationReceipt
+) -> bool:
+    """Only explicitly authorized newer work may supersede a terminal receipt."""
+
+    stage = attempt_dir / "staging.json"
+    return (
+        stage.exists()
+        and read_json(stage).get("supersedes_receipt_sha256")
+        == receipt.unreviewed_sha256
+    )
+
+
+# --- Promotion ----------------------------------------------------------------
+
+
+@dataclass
+class PromotionPlan:
+    target: Optional[QualificationReceipt]
+    ready_branch: Optional[str]
+    close_branches: list[str] = field(default_factory=list)
+    reason: str = ""
+    #: The qualified commit the ready branch must still be at when promoted.
+    ready_commit: Optional[str] = None
+
+
+def plan_promotion(
+    receipts: Iterable[QualificationReceipt],
+    bundle: Mapping[str, Any],
+    open_candidate_branches: Mapping[str, str],
+) -> PromotionPlan:
+    """What to do with candidate PRs given the ledger and ``main``'s certificate.
+
+    ``open_candidate_branches`` maps each open candidate branch to its head
+    commit. The target's branch is readied only while its head is exactly
+    the commit the target's qualification ran on
+    (``receipt.harness.git_commit``): the replayed and tested bytes are the
+    promoted bytes. The comparison certificate must also still equal main's;
+    otherwise the candidate needs a forced qualification of all gates against
+    the current baseline. A stale baseline cannot supersede other candidates.
+    A branch that moved since is left alone and reported.
+    """
+
+    receipts = list(receipts)
+    current = certified_order_key(bundle)
+    target = select_promotion_target(receipts, current)
+    baseline_matches = target is not None and (
+        target.previous_certificate == previous_certificate(bundle)
+    )
+    branch_pairs = {}
+    for receipt in receipts:
+        branch_pairs[candidate_branch(receipt.pair)] = receipt.pair.order_key
+    close = []
+    for branch in open_candidate_branches:
+        key = branch_pairs.get(branch)
+        if key is None:
+            continue
+        if key == current or dominates(current, key):
+            close.append(branch)
+        elif baseline_matches and dominates(target.pair.order_key, key):
+            close.append(branch)
+    if target is None:
+        return PromotionPlan(
+            target=None,
+            ready_branch=None,
+            close_branches=sorted(close),
+            reason=f"no promotable pair advances {current}",
+        )
+    if not baseline_matches:
+        return PromotionPlan(
+            target=target,
+            ready_branch=None,
+            close_branches=sorted(close),
+            reason=(
+                f"{target.pair.order_key} advances {current}, but its previous "
+                "certificate differs from current main; requalify with --force "
+                "against current main before promotion"
+            ),
+        )
+    branch = candidate_branch(target.pair)
+    qualified_commit = target.harness.git_commit
+    head = open_candidate_branches.get(branch)
+    if head is None:
+        return PromotionPlan(
+            target=target,
+            ready_branch=None,
+            close_branches=sorted(close),
+            reason=(
+                f"{target.pair.order_key} advances {current}, but its branch "
+                f"{branch} has no open pull request"
+            ),
+        )
+    if head != qualified_commit:
+        return PromotionPlan(
+            target=target,
+            ready_branch=None,
+            close_branches=sorted(close),
+            reason=(
+                f"{target.pair.order_key} advances {current}, but {branch} is at "
+                f"{head[:12]}, not the qualified commit {qualified_commit[:12]}; "
+                "nothing is promoted"
+            ),
+        )
+    return PromotionPlan(
+        target=target,
+        ready_branch=branch,
+        ready_commit=qualified_commit,
+        close_branches=sorted(close),
+        reason=f"{target.pair.order_key} advances {current}",
+    )
+
+
+def candidate_pr_body(
+    receipt: Optional[QualificationReceipt],
+    pair: CandidatePair,
+    *,
+    ledger_url: str,
+    prepared: Optional[Mapping[str, Any]] = None,
+) -> str:
+    """Markdown body for a candidate PR: the pair, the gates and the verdict."""
+
+    lines = [
+        f"Automated certification candidate for **policyengine-uk {pair.model.version}**, "
+        f"**policyengine-core {pair.core.version}** and **uk-data {pair.data.release_version}** "
+        f"(`{pair.data.dataset}`, sha256 `{pair.data.dataset_sha256[:16]}…`).",
+        "",
+        f"- Pair id: `{pair.pair_id}`",
+        f"- Ledger: {ledger_url}",
+    ]
+    if prepared:
+        lines.append(f"- Compatibility basis: `{prepared.get('compatibility_basis')}`")
+    lines += ["", "| Gate | Status | Summary |", "|---|---|---|"]
+    gates = {gate.gate_id: gate for gate in (receipt.gates if receipt else [])}
+    for gate_id in REQUIRED_GATES:
+        gate = gates.get(gate_id)
+        if gate is None:
+            lines.append(f"| `{gate_id}` | pending | |")
+        else:
+            summary = gate.summary.replace("|", "\\|").replace("\n", " ")
+            lines.append(f"| `{gate_id}` | {gate.status} | {summary} |")
+    verdict = receipt.verdict if receipt else "pending"
+    lines += [
+        "",
+        f"**Verdict: {verdict}.** Merging this PR promotes the pair. Until cos d1344 "
+        "is ruled, promotion is a reviewed merge; the certification evidence test "
+        "refuses an exact-pair certificate without a promotable ledger receipt.",
+        "",
+        "🤖 Generated with [Claude Code](https://claude.com/claude-code)",
+    ]
+    return "\n".join(lines) + "\n"
+
+
+# --- Release dispatch to consumers ----------------------------------------------
+
+
+def release_bundle_identity(
+    bundle: Mapping[str, Any], country_id: str = "uk"
+) -> dict[str, Any]:
+    """The certified pair a release carries, as consumers verify it."""
+
+    record = bundle["data_releases"][country_id]
+    certification = record.get("certification") or {}
+    model_package = bundle["countries"][country_id]["model_package"]
+    return {
+        "model_version": record["model_package"]["version"],
+        "model_wheel_sha256": record["model_package"].get("sha256"),
+        "core_version": bundle["packages"][CORE_PACKAGE]["version"],
+        "data_release_version": record["data_package"]["version"],
+        "dataset": record["default_dataset"],
+        "dataset_sha256": record["certified_data_artifact"].get("sha256"),
+        "certificate_sha256": certificate_sha256(record),
+        "compatibility_basis": certification.get("compatibility_basis"),
+        "model_package": model_package,
+    }
+
+
+def release_dispatch_payload(
+    bundle: Mapping[str, Any],
+    *,
+    version: str,
+    repository: str = "PolicyEngine/policyengine.py",
+    fetch_receipt: Optional[
+        Callable[[str, str], Optional[QualificationReceipt]]
+    ] = None,
+) -> dict[str, Any]:
+    """``policyengine-release`` repository dispatch for consumers.
+
+    ``client_payload.version`` stays the first field, as the existing
+    sim-api and api updaters read only it.
+    """
+
+    if bundle.get("policyengine_version") != version:
+        raise ValueError(
+            f"The bundle manifest is for {bundle.get('policyengine_version')}, not {version}."
+        )
+    identity = release_bundle_identity(bundle)
+    qualification = None
+    certification = bundle["data_releases"]["uk"].get("certification") or {}
+    pair_id = certification.get("qualification_pair_id")
+    if pair_id:
+        receipt = fetch_receipt("uk", pair_id) if fetch_receipt is not None else None
+        qualification = {
+            "uk": {
+                "pair_id": pair_id,
+                "verdict": receipt.verdict if receipt is not None else None,
+                # needs_review pairs are promotable only with an acceptance of
+                # exactly that receipt; consumers refuse promotable=false.
+                "promotable": receipt.promotable if receipt is not None else None,
+                "receipt_sha256": receipt.receipt_sha256
+                if receipt is not None
+                else None,
+            }
+        }
+    return {
+        "event_type": "policyengine-release",
+        "client_payload": {
+            "version": version,
+            "bundle": {"uk": identity},
+            "qualification": qualification,
+            "release_url": f"https://github.com/{repository}/releases/tag/{version}",
+        },
+    }
+
+
+def released_bundle_request(
+    bundle: Mapping[str, Any],
+    *,
+    version: str,
+    wheel_sha256: str,
+    packaged_manifest_sha256: str,
+    release_bundle: Mapping[str, Optional[str]],
+    hf_file: Callable[[str, str, str], bytes],
+    pypi_project: Callable[[str], Mapping[str, Any]],
+    requested_by: Mapping[str, Any],
+) -> ReplayQualificationRequest:
+    """``released_bundle`` replay request for a published policyengine release."""
+
+    record = bundle["data_releases"]["uk"]
+    data_package = record["data_package"]
+    manifest_bytes = hf_file(
+        data_package["repo_id"],
+        data_package["release_manifest_revision"],
+        data_package["release_manifest_path"],
+    )
+    manifest = DataReleaseManifest.model_validate_json(manifest_bytes)
+    dataset = record["default_dataset"]
+    artifact = manifest.artifacts[dataset]
+    reference = record["datasets"][dataset]
+    if reference.get("sha256") != artifact.sha256:
+        raise ValueError(
+            "The release manifest's dataset hash differs from the bundle's."
+        )
+    core_version = bundle["packages"][CORE_PACKAGE]["version"]
+    core_wheel = _wheel(
+        pypi_project(CORE_PACKAGE)["releases"][core_version], CORE_PACKAGE, core_version
+    )
+    return ReplayQualificationRequest.model_validate(
+        {
+            "schema": "policyengine.uk-replay-qualification-request/v1",
+            "kind": "released_bundle",
+            "attempt": 1,
+            "candidate": {
+                "policyengine": {
+                    "version": version,
+                    "source": {"pypi": {"wheel_sha256": wheel_sha256}},
+                    "packaged_bundle_manifest_sha256": packaged_manifest_sha256,
+                },
+                "policyengine_uk": {
+                    "version": record["model_package"]["version"],
+                    "wheel_sha256": record["model_package"]["sha256"],
+                },
+                "policyengine_core": {
+                    "version": core_version,
+                    "wheel_sha256": core_wheel.sha256,
+                },
+                "data": {
+                    "build_id": record["certified_data_artifact"].get("build_id")
+                    or record["build_id"],
+                    "dataset": dataset,
+                    "repo_id": reference.get("repo_id") or data_package["repo_id"],
+                    "repo_type": reference.get("repo_type")
+                    or data_package.get("repo_type", "model"),
+                    "revision": reference["revision"],
+                    "path": reference["path"],
+                    "sha256": reference["sha256"],
+                    "size_bytes": artifact.size_bytes,
+                    "release_manifest_sha256": hashlib.sha256(
+                        manifest_bytes
+                    ).hexdigest(),
+                },
+                "expected_release_bundle": dict(release_bundle),
+            },
+            "compare_to": "latest_certified",
+            "requested_by": dict(requested_by),
+        }
+    )
diff --git a/tests/fixtures/uk_contract/README.md b/tests/fixtures/uk_contract/README.md
new file mode 100644
index 00000000..66867a1f
--- /dev/null
+++ b/tests/fixtures/uk_contract/README.md
@@ -0,0 +1,15 @@
+# UK Budget reform contract reports
+
+Real reports from the UK Budget reform contract runner (workstream W1,
+`policyengine.tax_benefit_models.uk.budget_reform_contract`, PolicyEngine/policyengine.py#577),
+run on 11 October 2026. The qualification gate tests in `tests/test_uk_candidate_gates.py` read them.
+
+- `pinned_uk_2.125.1_defects.json`: every case on policyengine-uk 2.125.1, the pinned model at the time, with `max_examples=2` and no dataset. The 66 failures and 35 errors are the dated-reform defects the contract was written to catch.
+- `full_released_uk_2.130.1.json`: the full run of contract 1.1.0 (16 October 2026), the runner at policyengine.py#574's head merged with main 8a303636, on the certified pair policyengine-uk 2.130.1 with core 3.33.0: every route and case, seed 0, `max_examples=20`, population checks on the certified `enhanced_frs_2024_25` (uk-data 1.58.1, SHA-256 `056e80fd…`). Result: 792 pass, 25 pending a methodology call (d1344 and policyengine-uk#2396), 101 unsupported with reasons, both properties passing, 19 of 19 population rows; 29.6 minutes, peak RSS 3.5 GB. Its `versions` record policyengine 6.4.0, the stale metadata of the editable install that ran it; treat them as fixture metadata. The gate tests read it through the coverage the current runner module declares, and fail rather than skip when the runner's contract version moves past it. It replaces the 1.0.0 run on policyengine-uk 2.126.12.
+- `population_pass_max_examples_0.json`: population checks only, on the earlier certified `enhanced_frs_2024_25` (uk-data 1.58.0, SHA-256 `5ba399ea…`). It has no cases, and with `max_examples=0` its sampled properties did not run, so the gate treats it as an incomplete run.
+
+The edits:
+- `dataset.path` (a local Hugging Face cache path) was replaced with a placeholder in the two files that name a dataset, and every file was re-serialized with sorted keys.
+- Population rows are computed on the survey dataset, so a count from 1 to 9 in their `detail` is a small cell, which the metrics tool's `privacy.py` rule never publishes. In `full_released_uk_2.130.1.json`, `2027_people_with_lower_tax` and `payments_lower` (three rows each) are replaced by `"fewer than 10"`. No gate rule reads `detail`, and the qualification ledger (`publishable_contract_report`) publishes none of a population detail's values beyond its top-level true/false findings, since rows of one check on different routes share a subsample and their differences would give a handful's figures back. Check any new fixture for such counts before committing it.
+
+The run of contract 1.0.0 on policyengine-uk 2.126.8 (`full_allpass_uk_2.126.8.json`) was removed once no test read it.
diff --git a/tests/fixtures/uk_contract/full_released_uk_2.130.1.json b/tests/fixtures/uk_contract/full_released_uk_2.130.1.json
new file mode 100644
index 00000000..c2e15f68
--- /dev/null
+++ b/tests/fixtures/uk_contract/full_released_uk_2.130.1.json
@@ -0,0 +1,14922 @@
+{
+  "cases": [
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026:0",
+      "case_id": "april_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "unchanged before 2027-28",
+      "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "9f0a954308ede698ebada30899f04339b96ed509cb4ebe07c3885c229a9645f4",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.21,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:1",
+      "case_id": "april_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.21,
+      "holds": null,
+      "oracle": "in force on 30 April 2027",
+      "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "9f0a954308ede698ebada30899f04339b96ed509cb4ebe07c3885c229a9645f4",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.21,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028:2",
+      "case_id": "april_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "decision_id": null,
+      "expected": 0.21,
+      "holds": null,
+      "oracle": "held from 6 April 2027",
+      "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "9f0a954308ede698ebada30899f04339b96ed509cb4ebe07c3885c229a9645f4",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:income_tax:2026:3",
+      "case_id": "april_rate_change",
+      "check": "change:income_tax:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "9f0a954308ede698ebada30899f04339b96ed509cb4ebe07c3885c229a9645f4",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 174.300048828125,
+      "assertion_id": "change:income_tax:2027:4",
+      "case_id": "april_rate_change",
+      "check": "change:income_tax:2027",
+      "decision_id": null,
+      "expected": 174.29999999999967,
+      "holds": null,
+      "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "9f0a954308ede698ebada30899f04339b96ed509cb4ebe07c3885c229a9645f4",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 174.300048828125,
+      "assertion_id": "change:income_tax:2028:5",
+      "case_id": "april_rate_change",
+      "check": "change:income_tax:2028",
+      "decision_id": null,
+      "expected": 174.29999999999967,
+      "holds": null,
+      "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "9f0a954308ede698ebada30899f04339b96ed509cb4ebe07c3885c229a9645f4",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "baseline_isolation:income_tax:2026",
+      "expected": 3486.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "9f0a954308ede698ebada30899f04339b96ed509cb4ebe07c3885c229a9645f4",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "baseline_isolation:income_tax:2027",
+      "expected": 3486.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "9f0a954308ede698ebada30899f04339b96ed509cb4ebe07c3885c229a9645f4",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "baseline_isolation:income_tax:2028",
+      "expected": 3486.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "9f0a954308ede698ebada30899f04339b96ed509cb4ebe07c3885c229a9645f4",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "9f0a954308ede698ebada30899f04339b96ed509cb4ebe07c3885c229a9645f4",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "9f0a954308ede698ebada30899f04339b96ed509cb4ebe07c3885c229a9645f4",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "9f0a954308ede698ebada30899f04339b96ed509cb4ebe07c3885c229a9645f4",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:income_tax:2026",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "9f0a954308ede698ebada30899f04339b96ed509cb4ebe07c3885c229a9645f4",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:income_tax:2027",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "9f0a954308ede698ebada30899f04339b96ed509cb4ebe07c3885c229a9645f4",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:income_tax:2028",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "9f0a954308ede698ebada30899f04339b96ed509cb4ebe07c3885c229a9645f4",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026:0",
+      "case_id": "april_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "unchanged before 2027-28",
+      "reform_digest": "c24ddebf09ef159ad38ebd530d2deca18d2ec7165400a6d92ada5c9d6175377e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "979484c0e230b3255e62c00e7bad677dcf6b7839f97ff732a647828c0f14f46e",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.21,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:1",
+      "case_id": "april_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.21,
+      "holds": null,
+      "oracle": "in force on 30 April 2027",
+      "reform_digest": "c24ddebf09ef159ad38ebd530d2deca18d2ec7165400a6d92ada5c9d6175377e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "979484c0e230b3255e62c00e7bad677dcf6b7839f97ff732a647828c0f14f46e",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.21,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028:2",
+      "case_id": "april_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "decision_id": null,
+      "expected": 0.21,
+      "holds": null,
+      "oracle": "held from 6 April 2027",
+      "reform_digest": "c24ddebf09ef159ad38ebd530d2deca18d2ec7165400a6d92ada5c9d6175377e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "979484c0e230b3255e62c00e7bad677dcf6b7839f97ff732a647828c0f14f46e",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:income_tax:2026:3",
+      "case_id": "april_rate_change",
+      "check": "change:income_tax:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "c24ddebf09ef159ad38ebd530d2deca18d2ec7165400a6d92ada5c9d6175377e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "979484c0e230b3255e62c00e7bad677dcf6b7839f97ff732a647828c0f14f46e",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 174.300048828125,
+      "assertion_id": "change:income_tax:2027:4",
+      "case_id": "april_rate_change",
+      "check": "change:income_tax:2027",
+      "decision_id": null,
+      "expected": 174.29999999999967,
+      "holds": null,
+      "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "c24ddebf09ef159ad38ebd530d2deca18d2ec7165400a6d92ada5c9d6175377e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "979484c0e230b3255e62c00e7bad677dcf6b7839f97ff732a647828c0f14f46e",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 174.300048828125,
+      "assertion_id": "change:income_tax:2028:5",
+      "case_id": "april_rate_change",
+      "check": "change:income_tax:2028",
+      "decision_id": null,
+      "expected": 174.29999999999967,
+      "holds": null,
+      "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "c24ddebf09ef159ad38ebd530d2deca18d2ec7165400a6d92ada5c9d6175377e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "979484c0e230b3255e62c00e7bad677dcf6b7839f97ff732a647828c0f14f46e",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "baseline_isolation:income_tax:2026",
+      "expected": 3486.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "c24ddebf09ef159ad38ebd530d2deca18d2ec7165400a6d92ada5c9d6175377e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "979484c0e230b3255e62c00e7bad677dcf6b7839f97ff732a647828c0f14f46e",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "baseline_isolation:income_tax:2027",
+      "expected": 3486.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "c24ddebf09ef159ad38ebd530d2deca18d2ec7165400a6d92ada5c9d6175377e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "979484c0e230b3255e62c00e7bad677dcf6b7839f97ff732a647828c0f14f46e",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "baseline_isolation:income_tax:2028",
+      "expected": 3486.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "c24ddebf09ef159ad38ebd530d2deca18d2ec7165400a6d92ada5c9d6175377e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "979484c0e230b3255e62c00e7bad677dcf6b7839f97ff732a647828c0f14f46e",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "c24ddebf09ef159ad38ebd530d2deca18d2ec7165400a6d92ada5c9d6175377e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "979484c0e230b3255e62c00e7bad677dcf6b7839f97ff732a647828c0f14f46e",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "c24ddebf09ef159ad38ebd530d2deca18d2ec7165400a6d92ada5c9d6175377e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "979484c0e230b3255e62c00e7bad677dcf6b7839f97ff732a647828c0f14f46e",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "c24ddebf09ef159ad38ebd530d2deca18d2ec7165400a6d92ada5c9d6175377e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "979484c0e230b3255e62c00e7bad677dcf6b7839f97ff732a647828c0f14f46e",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:income_tax:2026",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "c24ddebf09ef159ad38ebd530d2deca18d2ec7165400a6d92ada5c9d6175377e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "979484c0e230b3255e62c00e7bad677dcf6b7839f97ff732a647828c0f14f46e",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:income_tax:2027",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "c24ddebf09ef159ad38ebd530d2deca18d2ec7165400a6d92ada5c9d6175377e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "979484c0e230b3255e62c00e7bad677dcf6b7839f97ff732a647828c0f14f46e",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:income_tax:2028",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "c24ddebf09ef159ad38ebd530d2deca18d2ec7165400a6d92ada5c9d6175377e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "979484c0e230b3255e62c00e7bad677dcf6b7839f97ff732a647828c0f14f46e",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026:0",
+      "case_id": "april_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "unchanged before 2027-28",
+      "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.21,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:1",
+      "case_id": "april_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.21,
+      "holds": null,
+      "oracle": "in force on 30 April 2027",
+      "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.21,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028:2",
+      "case_id": "april_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "decision_id": null,
+      "expected": 0.21,
+      "holds": null,
+      "oracle": "held from 6 April 2027",
+      "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:income_tax:2026:3",
+      "case_id": "april_rate_change",
+      "check": "change:income_tax:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 174.300048828125,
+      "assertion_id": "change:income_tax:2027:4",
+      "case_id": "april_rate_change",
+      "check": "change:income_tax:2027",
+      "decision_id": null,
+      "expected": 174.29999999999967,
+      "holds": null,
+      "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 174.300048828125,
+      "assertion_id": "change:income_tax:2028:5",
+      "case_id": "april_rate_change",
+      "check": "change:income_tax:2028",
+      "decision_id": null,
+      "expected": 174.29999999999967,
+      "holds": null,
+      "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "baseline_isolation:income_tax:2026",
+      "expected": 3486.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "baseline_isolation:income_tax:2027",
+      "expected": 3486.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "baseline_isolation:income_tax:2028",
+      "expected": 3486.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:income_tax:2026",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:income_tax:2027",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:income_tax:2028",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026:0",
+      "case_id": "april_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f504a06f3382203f96a08fc1c07b86a0f001d4b40c0a2e805f6c0c4d29c50221",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "e9c1d3beb83179ffb9bb369d900a2f264eda9535f67431adcc31bf5f03ca86b1",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:1",
+      "case_id": "april_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f504a06f3382203f96a08fc1c07b86a0f001d4b40c0a2e805f6c0c4d29c50221",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "e9c1d3beb83179ffb9bb369d900a2f264eda9535f67431adcc31bf5f03ca86b1",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028:2",
+      "case_id": "april_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f504a06f3382203f96a08fc1c07b86a0f001d4b40c0a2e805f6c0c4d29c50221",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "e9c1d3beb83179ffb9bb369d900a2f264eda9535f67431adcc31bf5f03ca86b1",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:income_tax:2026:3",
+      "case_id": "april_rate_change",
+      "check": "change:income_tax:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "f504a06f3382203f96a08fc1c07b86a0f001d4b40c0a2e805f6c0c4d29c50221",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "e9c1d3beb83179ffb9bb369d900a2f264eda9535f67431adcc31bf5f03ca86b1",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 174.300048828125,
+      "assertion_id": "change:income_tax:2027:4",
+      "case_id": "april_rate_change",
+      "check": "change:income_tax:2027",
+      "decision_id": null,
+      "expected": 174.29999999999967,
+      "holds": null,
+      "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "f504a06f3382203f96a08fc1c07b86a0f001d4b40c0a2e805f6c0c4d29c50221",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "e9c1d3beb83179ffb9bb369d900a2f264eda9535f67431adcc31bf5f03ca86b1",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 174.300048828125,
+      "assertion_id": "change:income_tax:2028:5",
+      "case_id": "april_rate_change",
+      "check": "change:income_tax:2028",
+      "decision_id": null,
+      "expected": 174.29999999999967,
+      "holds": null,
+      "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "f504a06f3382203f96a08fc1c07b86a0f001d4b40c0a2e805f6c0c4d29c50221",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "e9c1d3beb83179ffb9bb369d900a2f264eda9535f67431adcc31bf5f03ca86b1",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:income_tax:2026",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "f504a06f3382203f96a08fc1c07b86a0f001d4b40c0a2e805f6c0c4d29c50221",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "e9c1d3beb83179ffb9bb369d900a2f264eda9535f67431adcc31bf5f03ca86b1",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:income_tax:2027",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "f504a06f3382203f96a08fc1c07b86a0f001d4b40c0a2e805f6c0c4d29c50221",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "e9c1d3beb83179ffb9bb369d900a2f264eda9535f67431adcc31bf5f03ca86b1",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:income_tax:2028",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "f504a06f3382203f96a08fc1c07b86a0f001d4b40c0a2e805f6c0c4d29c50221",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "e9c1d3beb83179ffb9bb369d900a2f264eda9535f67431adcc31bf5f03ca86b1",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026:0",
+      "case_id": "april_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "cd54b3b3b30cfd71448cbeb374c5d31aedca3d52733ab5c81e542feaba80704c",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "e9c1d3beb83179ffb9bb369d900a2f264eda9535f67431adcc31bf5f03ca86b1",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:1",
+      "case_id": "april_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "cd54b3b3b30cfd71448cbeb374c5d31aedca3d52733ab5c81e542feaba80704c",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "e9c1d3beb83179ffb9bb369d900a2f264eda9535f67431adcc31bf5f03ca86b1",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028:2",
+      "case_id": "april_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "cd54b3b3b30cfd71448cbeb374c5d31aedca3d52733ab5c81e542feaba80704c",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "e9c1d3beb83179ffb9bb369d900a2f264eda9535f67431adcc31bf5f03ca86b1",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:income_tax:2026:3",
+      "case_id": "april_rate_change",
+      "check": "change:income_tax:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "cd54b3b3b30cfd71448cbeb374c5d31aedca3d52733ab5c81e542feaba80704c",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "e9c1d3beb83179ffb9bb369d900a2f264eda9535f67431adcc31bf5f03ca86b1",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 174.300048828125,
+      "assertion_id": "change:income_tax:2027:4",
+      "case_id": "april_rate_change",
+      "check": "change:income_tax:2027",
+      "decision_id": null,
+      "expected": 174.29999999999967,
+      "holds": null,
+      "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "cd54b3b3b30cfd71448cbeb374c5d31aedca3d52733ab5c81e542feaba80704c",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "e9c1d3beb83179ffb9bb369d900a2f264eda9535f67431adcc31bf5f03ca86b1",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 174.300048828125,
+      "assertion_id": "change:income_tax:2028:5",
+      "case_id": "april_rate_change",
+      "check": "change:income_tax:2028",
+      "decision_id": null,
+      "expected": 174.29999999999967,
+      "holds": null,
+      "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "cd54b3b3b30cfd71448cbeb374c5d31aedca3d52733ab5c81e542feaba80704c",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "e9c1d3beb83179ffb9bb369d900a2f264eda9535f67431adcc31bf5f03ca86b1",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:income_tax:2026",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "cd54b3b3b30cfd71448cbeb374c5d31aedca3d52733ab5c81e542feaba80704c",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "e9c1d3beb83179ffb9bb369d900a2f264eda9535f67431adcc31bf5f03ca86b1",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:income_tax:2027",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "cd54b3b3b30cfd71448cbeb374c5d31aedca3d52733ab5c81e542feaba80704c",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "e9c1d3beb83179ffb9bb369d900a2f264eda9535f67431adcc31bf5f03ca86b1",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "april_rate_change",
+      "check": "unreformed_after_reform:income_tax:2028",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "cd54b3b3b30cfd71448cbeb374c5d31aedca3d52733ab5c81e542feaba80704c",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_rate_change",
+        "reform_sha256": "e9c1d3beb83179ffb9bb369d900a2f264eda9535f67431adcc31bf5f03ca86b1",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "case_id": "april_rate_change",
+      "check": null,
+      "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+      "reform_digest": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "the route does not run in-process",
+      "route": "api.sim_api",
+      "status": "unsupported"
+    },
+    {
+      "actual": 0.21,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:0",
+      "case_id": "finite_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.21,
+      "holds": null,
+      "oracle": "in force on 30 April 2027",
+      "reform_digest": "5a0ca24b82794f7c32123733efa38f90aa602676efa5219e4a13d97ff46f69ea",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "1435fd005a02984184442093db0558b51cf444c5a9e03bdbd4a2279e4b135105",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028:1",
+      "case_id": "finite_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "prior law after 5 April 2028",
+      "reform_digest": "5a0ca24b82794f7c32123733efa38f90aa602676efa5219e4a13d97ff46f69ea",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "1435fd005a02984184442093db0558b51cf444c5a9e03bdbd4a2279e4b135105",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 174.300048828125,
+      "assertion_id": "change:income_tax:2027:2",
+      "case_id": "finite_rate_change",
+      "check": "change:income_tax:2027",
+      "decision_id": null,
+      "expected": 174.29999999999967,
+      "holds": null,
+      "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "5a0ca24b82794f7c32123733efa38f90aa602676efa5219e4a13d97ff46f69ea",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "1435fd005a02984184442093db0558b51cf444c5a9e03bdbd4a2279e4b135105",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:income_tax:2028:3",
+      "case_id": "finite_rate_change",
+      "check": "change:income_tax:2028",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "the change has ended",
+      "reform_digest": "5a0ca24b82794f7c32123733efa38f90aa602676efa5219e4a13d97ff46f69ea",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "1435fd005a02984184442093db0558b51cf444c5a9e03bdbd4a2279e4b135105",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "finite_rate_change",
+      "check": "baseline_isolation:income_tax:2027",
+      "expected": 3486.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "5a0ca24b82794f7c32123733efa38f90aa602676efa5219e4a13d97ff46f69ea",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "1435fd005a02984184442093db0558b51cf444c5a9e03bdbd4a2279e4b135105",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "finite_rate_change",
+      "check": "baseline_isolation:income_tax:2028",
+      "expected": 3486.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "5a0ca24b82794f7c32123733efa38f90aa602676efa5219e4a13d97ff46f69ea",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "1435fd005a02984184442093db0558b51cf444c5a9e03bdbd4a2279e4b135105",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "finite_rate_change",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "5a0ca24b82794f7c32123733efa38f90aa602676efa5219e4a13d97ff46f69ea",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "1435fd005a02984184442093db0558b51cf444c5a9e03bdbd4a2279e4b135105",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "finite_rate_change",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "5a0ca24b82794f7c32123733efa38f90aa602676efa5219e4a13d97ff46f69ea",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "1435fd005a02984184442093db0558b51cf444c5a9e03bdbd4a2279e4b135105",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "finite_rate_change",
+      "check": "unreformed_after_reform:income_tax:2027",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "5a0ca24b82794f7c32123733efa38f90aa602676efa5219e4a13d97ff46f69ea",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "1435fd005a02984184442093db0558b51cf444c5a9e03bdbd4a2279e4b135105",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "finite_rate_change",
+      "check": "unreformed_after_reform:income_tax:2028",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "5a0ca24b82794f7c32123733efa38f90aa602676efa5219e4a13d97ff46f69ea",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "1435fd005a02984184442093db0558b51cf444c5a9e03bdbd4a2279e4b135105",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.21,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:0",
+      "case_id": "finite_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.21,
+      "holds": null,
+      "oracle": "in force on 30 April 2027",
+      "reform_digest": "5e2b0da064082c7c363dec99c7e5bab2f190ae2f5ff9633454f1e10223293323",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "ae43ea29444561c5c77d32ad9834b0075bd62c00d605154065a304dd0055e229",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028:1",
+      "case_id": "finite_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "prior law after 5 April 2028",
+      "reform_digest": "5e2b0da064082c7c363dec99c7e5bab2f190ae2f5ff9633454f1e10223293323",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "ae43ea29444561c5c77d32ad9834b0075bd62c00d605154065a304dd0055e229",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 174.300048828125,
+      "assertion_id": "change:income_tax:2027:2",
+      "case_id": "finite_rate_change",
+      "check": "change:income_tax:2027",
+      "decision_id": null,
+      "expected": 174.29999999999967,
+      "holds": null,
+      "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "5e2b0da064082c7c363dec99c7e5bab2f190ae2f5ff9633454f1e10223293323",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "ae43ea29444561c5c77d32ad9834b0075bd62c00d605154065a304dd0055e229",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:income_tax:2028:3",
+      "case_id": "finite_rate_change",
+      "check": "change:income_tax:2028",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "the change has ended",
+      "reform_digest": "5e2b0da064082c7c363dec99c7e5bab2f190ae2f5ff9633454f1e10223293323",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "ae43ea29444561c5c77d32ad9834b0075bd62c00d605154065a304dd0055e229",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "finite_rate_change",
+      "check": "baseline_isolation:income_tax:2027",
+      "expected": 3486.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "5e2b0da064082c7c363dec99c7e5bab2f190ae2f5ff9633454f1e10223293323",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "ae43ea29444561c5c77d32ad9834b0075bd62c00d605154065a304dd0055e229",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "finite_rate_change",
+      "check": "baseline_isolation:income_tax:2028",
+      "expected": 3486.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "5e2b0da064082c7c363dec99c7e5bab2f190ae2f5ff9633454f1e10223293323",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "ae43ea29444561c5c77d32ad9834b0075bd62c00d605154065a304dd0055e229",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "finite_rate_change",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "5e2b0da064082c7c363dec99c7e5bab2f190ae2f5ff9633454f1e10223293323",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "ae43ea29444561c5c77d32ad9834b0075bd62c00d605154065a304dd0055e229",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "finite_rate_change",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "5e2b0da064082c7c363dec99c7e5bab2f190ae2f5ff9633454f1e10223293323",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "ae43ea29444561c5c77d32ad9834b0075bd62c00d605154065a304dd0055e229",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "finite_rate_change",
+      "check": "unreformed_after_reform:income_tax:2027",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "5e2b0da064082c7c363dec99c7e5bab2f190ae2f5ff9633454f1e10223293323",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "ae43ea29444561c5c77d32ad9834b0075bd62c00d605154065a304dd0055e229",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "finite_rate_change",
+      "check": "unreformed_after_reform:income_tax:2028",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "5e2b0da064082c7c363dec99c7e5bab2f190ae2f5ff9633454f1e10223293323",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "ae43ea29444561c5c77d32ad9834b0075bd62c00d605154065a304dd0055e229",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.21,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:0",
+      "case_id": "finite_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.21,
+      "holds": null,
+      "oracle": "in force on 30 April 2027",
+      "reform_digest": "ed4782036ee619f8c07483cac573d44bf1917fa33d3fa49a8551eebb632852ef",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028:1",
+      "case_id": "finite_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "prior law after 5 April 2028",
+      "reform_digest": "ed4782036ee619f8c07483cac573d44bf1917fa33d3fa49a8551eebb632852ef",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 174.300048828125,
+      "assertion_id": "change:income_tax:2027:2",
+      "case_id": "finite_rate_change",
+      "check": "change:income_tax:2027",
+      "decision_id": null,
+      "expected": 174.29999999999967,
+      "holds": null,
+      "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "ed4782036ee619f8c07483cac573d44bf1917fa33d3fa49a8551eebb632852ef",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:income_tax:2028:3",
+      "case_id": "finite_rate_change",
+      "check": "change:income_tax:2028",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "the change has ended",
+      "reform_digest": "ed4782036ee619f8c07483cac573d44bf1917fa33d3fa49a8551eebb632852ef",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "finite_rate_change",
+      "check": "baseline_isolation:income_tax:2027",
+      "expected": 3486.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "ed4782036ee619f8c07483cac573d44bf1917fa33d3fa49a8551eebb632852ef",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "finite_rate_change",
+      "check": "baseline_isolation:income_tax:2028",
+      "expected": 3486.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "ed4782036ee619f8c07483cac573d44bf1917fa33d3fa49a8551eebb632852ef",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "finite_rate_change",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "ed4782036ee619f8c07483cac573d44bf1917fa33d3fa49a8551eebb632852ef",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "finite_rate_change",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "ed4782036ee619f8c07483cac573d44bf1917fa33d3fa49a8551eebb632852ef",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "finite_rate_change",
+      "check": "unreformed_after_reform:income_tax:2027",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "ed4782036ee619f8c07483cac573d44bf1917fa33d3fa49a8551eebb632852ef",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "finite_rate_change",
+      "check": "unreformed_after_reform:income_tax:2028",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "ed4782036ee619f8c07483cac573d44bf1917fa33d3fa49a8551eebb632852ef",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:0",
+      "case_id": "finite_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "97674d142cf5719ae87a2523e758805be566fe02241aeff2a658fee27235c3f8",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "9413545a91d0019e0b4c06272d040c0948378d600c25eba395341396be96f98a",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028:1",
+      "case_id": "finite_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "97674d142cf5719ae87a2523e758805be566fe02241aeff2a658fee27235c3f8",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "9413545a91d0019e0b4c06272d040c0948378d600c25eba395341396be96f98a",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "actual": 174.300048828125,
+      "assertion_id": "change:income_tax:2027:2",
+      "case_id": "finite_rate_change",
+      "check": "change:income_tax:2027",
+      "decision_id": null,
+      "expected": 174.29999999999967,
+      "holds": null,
+      "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "97674d142cf5719ae87a2523e758805be566fe02241aeff2a658fee27235c3f8",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "9413545a91d0019e0b4c06272d040c0948378d600c25eba395341396be96f98a",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:income_tax:2028:3",
+      "case_id": "finite_rate_change",
+      "check": "change:income_tax:2028",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "the change has ended",
+      "reform_digest": "97674d142cf5719ae87a2523e758805be566fe02241aeff2a658fee27235c3f8",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "9413545a91d0019e0b4c06272d040c0948378d600c25eba395341396be96f98a",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "finite_rate_change",
+      "check": "unreformed_after_reform:income_tax:2027",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "97674d142cf5719ae87a2523e758805be566fe02241aeff2a658fee27235c3f8",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "9413545a91d0019e0b4c06272d040c0948378d600c25eba395341396be96f98a",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "finite_rate_change",
+      "check": "unreformed_after_reform:income_tax:2028",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "97674d142cf5719ae87a2523e758805be566fe02241aeff2a658fee27235c3f8",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "9413545a91d0019e0b4c06272d040c0948378d600c25eba395341396be96f98a",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:0",
+      "case_id": "finite_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "635e34257a38637cabd07234afb9e37355921ce7b680a98d8f85a1e05ecbbb64",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "9413545a91d0019e0b4c06272d040c0948378d600c25eba395341396be96f98a",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028:1",
+      "case_id": "finite_rate_change",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "635e34257a38637cabd07234afb9e37355921ce7b680a98d8f85a1e05ecbbb64",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "9413545a91d0019e0b4c06272d040c0948378d600c25eba395341396be96f98a",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "actual": 174.300048828125,
+      "assertion_id": "change:income_tax:2027:2",
+      "case_id": "finite_rate_change",
+      "check": "change:income_tax:2027",
+      "decision_id": null,
+      "expected": 174.29999999999967,
+      "holds": null,
+      "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "635e34257a38637cabd07234afb9e37355921ce7b680a98d8f85a1e05ecbbb64",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "9413545a91d0019e0b4c06272d040c0948378d600c25eba395341396be96f98a",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:income_tax:2028:3",
+      "case_id": "finite_rate_change",
+      "check": "change:income_tax:2028",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "the change has ended",
+      "reform_digest": "635e34257a38637cabd07234afb9e37355921ce7b680a98d8f85a1e05ecbbb64",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "9413545a91d0019e0b4c06272d040c0948378d600c25eba395341396be96f98a",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "finite_rate_change",
+      "check": "unreformed_after_reform:income_tax:2027",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "635e34257a38637cabd07234afb9e37355921ce7b680a98d8f85a1e05ecbbb64",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "9413545a91d0019e0b4c06272d040c0948378d600c25eba395341396be96f98a",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 3486.0,
+      "case_id": "finite_rate_change",
+      "check": "unreformed_after_reform:income_tax:2028",
+      "expected": 3486.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "635e34257a38637cabd07234afb9e37355921ce7b680a98d8f85a1e05ecbbb64",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "finite_rate_change",
+        "reform_sha256": "9413545a91d0019e0b4c06272d040c0948378d600c25eba395341396be96f98a",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "case_id": "finite_rate_change",
+      "check": null,
+      "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+      "reform_digest": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "the route does not run in-process",
+      "route": "api.sim_api",
+      "status": "unsupported"
+    },
+    {
+      "actual": 12570.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031:0",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031",
+      "decision_id": null,
+      "expected": 12570.0,
+      "holds": null,
+      "oracle": "frozen",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 37700.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031:1",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031",
+      "decision_id": null,
+      "expected": 37700.0,
+      "holds": null,
+      "oracle": "frozen",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12570.0,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2031:2",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2031",
+      "decision_id": null,
+      "expected": 12570.0,
+      "holds": null,
+      "oracle": "aligned with the allowance",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 50270.0,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2031:3",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2031",
+      "decision_id": null,
+      "expected": 50270.0,
+      "holds": null,
+      "oracle": "allowance + basic rate limit",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 241.73,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031:4",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031",
+      "decision_id": null,
+      "expected": 241.73,
+      "holds": null,
+      "oracle": "12,570 / 52, to the penny",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 966.73,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2031:5",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2031",
+      "decision_id": null,
+      "expected": 966.73,
+      "holds": null,
+      "oracle": "50,270 / 52, to the penny",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 52.0,
+      "assertion_id": "change:income_tax:2031:6",
+      "case_id": "extended_freeze",
+      "check": "change:income_tax:2031",
+      "decision_id": null,
+      "expected": 52.0,
+      "holds": null,
+      "oracle": "basic rate x (current law's allowance - 12,570)",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12570.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2032:7",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2032",
+      "decision_id": null,
+      "expected": 12570.0,
+      "holds": null,
+      "oracle": "frozen",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 37700.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2032:8",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2032",
+      "decision_id": null,
+      "expected": 37700.0,
+      "holds": null,
+      "oracle": "frozen",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12570.0,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2032:9",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2032",
+      "decision_id": null,
+      "expected": 12570.0,
+      "holds": null,
+      "oracle": "aligned with the allowance",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 50270.0,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2032:10",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2032",
+      "decision_id": null,
+      "expected": 50270.0,
+      "holds": null,
+      "oracle": "allowance + basic rate limit",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 241.73,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2032:11",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2032",
+      "decision_id": null,
+      "expected": 241.73,
+      "holds": null,
+      "oracle": "12,570 / 52, to the penny",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 966.73,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2032:12",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2032",
+      "decision_id": null,
+      "expected": 966.73,
+      "holds": null,
+      "oracle": "50,270 / 52, to the penny",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 104.0,
+      "assertion_id": "change:income_tax:2032:13",
+      "case_id": "extended_freeze",
+      "check": "change:income_tax:2032",
+      "decision_id": null,
+      "expected": 104.0,
+      "holds": null,
+      "oracle": "basic rate x (current law's allowance - 12,570)",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12570.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2033:14",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2033",
+      "decision_id": null,
+      "expected": 12570.0,
+      "holds": null,
+      "oracle": "frozen",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 37700.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2033:15",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2033",
+      "decision_id": null,
+      "expected": 37700.0,
+      "holds": null,
+      "oracle": "frozen",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12570.0,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2033:16",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2033",
+      "decision_id": null,
+      "expected": 12570.0,
+      "holds": null,
+      "oracle": "aligned with the allowance",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 50270.0,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2033:17",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2033",
+      "decision_id": null,
+      "expected": 50270.0,
+      "holds": null,
+      "oracle": "allowance + basic rate limit",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 241.73,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2033:18",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2033",
+      "decision_id": null,
+      "expected": 241.73,
+      "holds": null,
+      "oracle": "12,570 / 52, to the penny",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 966.73,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2033:19",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2033",
+      "decision_id": null,
+      "expected": 966.73,
+      "holds": null,
+      "oracle": "50,270 / 52, to the penny",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 158.0,
+      "assertion_id": "change:income_tax:2033:20",
+      "case_id": "extended_freeze",
+      "check": "change:income_tax:2033",
+      "decision_id": null,
+      "expected": 158.0,
+      "holds": null,
+      "oracle": "basic rate x (current law's allowance - 12,570)",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13630.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034:21",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034",
+      "decision_id": null,
+      "expected": 13630.0,
+      "holds": null,
+      "oracle": "prior law's projected path resumes (PRIOR_LAW_OPTION)",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13630.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034:22",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034",
+      "decision_id": "d1344",
+      "expected": 12830.0,
+      "holds": false,
+      "oracle": "ITA 2007 s57 from the frozen \u00a312,570 with 2034's September CPI rise",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pending"
+    },
+    {
+      "actual": 41000.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2034:23",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2034",
+      "decision_id": "d1344",
+      "expected": 38500.0,
+      "holds": false,
+      "oracle": "ITA 2007 s21 from the frozen \u00a337,700 with 2034's September CPI rise",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pending"
+    },
+    {
+      "actual": 3424.39990234375,
+      "case_id": "extended_freeze",
+      "check": "baseline_isolation:income_tax:2031",
+      "expected": 3424.39990234375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3372.39990234375,
+      "case_id": "extended_freeze",
+      "check": "baseline_isolation:income_tax:2032",
+      "expected": 3372.39990234375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3318.39990234375,
+      "case_id": "extended_freeze",
+      "check": "baseline_isolation:income_tax:2033",
+      "expected": 3318.39990234375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12830.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031",
+      "expected": 12830.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 38500.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[1].threshold:2031",
+      "expected": 38500.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12830.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2031",
+      "expected": 12830.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 51330.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2031",
+      "expected": 51330.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 246.73,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031",
+      "expected": 246.73,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 987.12,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2031",
+      "expected": 987.12,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3424.39990234375,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:income_tax:2031",
+      "expected": 3424.39990234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13090.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.allowances.personal_allowance.amount:2032",
+      "expected": 13090.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 39300.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[1].threshold:2032",
+      "expected": 39300.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13090.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2032",
+      "expected": 13090.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 52390.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2032",
+      "expected": 52390.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 251.73,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2032",
+      "expected": 251.73,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1007.5,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2032",
+      "expected": 1007.5,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3372.39990234375,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:income_tax:2032",
+      "expected": 3372.39990234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13360.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.allowances.personal_allowance.amount:2033",
+      "expected": 13360.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 40100.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[1].threshold:2033",
+      "expected": 40100.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13360.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2033",
+      "expected": 13360.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 53460.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2033",
+      "expected": 53460.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 256.92,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2033",
+      "expected": 256.92,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1028.08,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2033",
+      "expected": 1028.08,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3318.39990234375,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:income_tax:2033",
+      "expected": 3318.39990234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13630.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034",
+      "expected": 13630.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 41000.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[1].threshold:2034",
+      "expected": 41000.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "1b943da8e8bc75c3d0e6d96ab325eaca5480e5798ad4dbc239037eec6371061c",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12570.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031:0",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031",
+      "decision_id": null,
+      "expected": 12570.0,
+      "holds": null,
+      "oracle": "frozen",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 37700.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031:1",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031",
+      "decision_id": null,
+      "expected": 37700.0,
+      "holds": null,
+      "oracle": "frozen",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 12570.0,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2031:2",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2031",
+      "decision_id": null,
+      "expected": 12570.0,
+      "holds": null,
+      "oracle": "aligned with the allowance",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 50270.0,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2031:3",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2031",
+      "decision_id": null,
+      "expected": 50270.0,
+      "holds": null,
+      "oracle": "allowance + basic rate limit",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 241.73,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031:4",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031",
+      "decision_id": null,
+      "expected": 241.73,
+      "holds": null,
+      "oracle": "12,570 / 52, to the penny",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 966.73,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2031:5",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2031",
+      "decision_id": null,
+      "expected": 966.73,
+      "holds": null,
+      "oracle": "50,270 / 52, to the penny",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 52.0,
+      "assertion_id": "change:income_tax:2031:6",
+      "case_id": "extended_freeze",
+      "check": "change:income_tax:2031",
+      "decision_id": null,
+      "expected": 52.0,
+      "holds": null,
+      "oracle": "basic rate x (current law's allowance - 12,570)",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 12570.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2032:7",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2032",
+      "decision_id": null,
+      "expected": 12570.0,
+      "holds": null,
+      "oracle": "frozen",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 37700.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2032:8",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2032",
+      "decision_id": null,
+      "expected": 37700.0,
+      "holds": null,
+      "oracle": "frozen",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 12570.0,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2032:9",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2032",
+      "decision_id": null,
+      "expected": 12570.0,
+      "holds": null,
+      "oracle": "aligned with the allowance",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 50270.0,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2032:10",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2032",
+      "decision_id": null,
+      "expected": 50270.0,
+      "holds": null,
+      "oracle": "allowance + basic rate limit",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 241.73,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2032:11",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2032",
+      "decision_id": null,
+      "expected": 241.73,
+      "holds": null,
+      "oracle": "12,570 / 52, to the penny",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 966.73,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2032:12",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2032",
+      "decision_id": null,
+      "expected": 966.73,
+      "holds": null,
+      "oracle": "50,270 / 52, to the penny",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 104.0,
+      "assertion_id": "change:income_tax:2032:13",
+      "case_id": "extended_freeze",
+      "check": "change:income_tax:2032",
+      "decision_id": null,
+      "expected": 104.0,
+      "holds": null,
+      "oracle": "basic rate x (current law's allowance - 12,570)",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 12570.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2033:14",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2033",
+      "decision_id": null,
+      "expected": 12570.0,
+      "holds": null,
+      "oracle": "frozen",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 37700.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2033:15",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2033",
+      "decision_id": null,
+      "expected": 37700.0,
+      "holds": null,
+      "oracle": "frozen",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 12570.0,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2033:16",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2033",
+      "decision_id": null,
+      "expected": 12570.0,
+      "holds": null,
+      "oracle": "aligned with the allowance",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 50270.0,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2033:17",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2033",
+      "decision_id": null,
+      "expected": 50270.0,
+      "holds": null,
+      "oracle": "allowance + basic rate limit",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 241.73,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2033:18",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2033",
+      "decision_id": null,
+      "expected": 241.73,
+      "holds": null,
+      "oracle": "12,570 / 52, to the penny",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 966.73,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2033:19",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2033",
+      "decision_id": null,
+      "expected": 966.73,
+      "holds": null,
+      "oracle": "50,270 / 52, to the penny",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 158.0,
+      "assertion_id": "change:income_tax:2033:20",
+      "case_id": "extended_freeze",
+      "check": "change:income_tax:2033",
+      "decision_id": null,
+      "expected": 158.0,
+      "holds": null,
+      "oracle": "basic rate x (current law's allowance - 12,570)",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 13630.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034:21",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034",
+      "decision_id": null,
+      "expected": 13630.0,
+      "holds": null,
+      "oracle": "prior law's projected path resumes (PRIOR_LAW_OPTION)",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 13630.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034:22",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034",
+      "decision_id": "d1344",
+      "expected": 12830.0,
+      "holds": false,
+      "oracle": "ITA 2007 s57 from the frozen \u00a312,570 with 2034's September CPI rise",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pending"
+    },
+    {
+      "actual": 41000.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2034:23",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2034",
+      "decision_id": "d1344",
+      "expected": 38500.0,
+      "holds": false,
+      "oracle": "ITA 2007 s21 from the frozen \u00a337,700 with 2034's September CPI rise",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pending"
+    },
+    {
+      "actual": 3424.39990234375,
+      "case_id": "extended_freeze",
+      "check": "baseline_isolation:income_tax:2031",
+      "expected": 3424.39990234375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 3372.39990234375,
+      "case_id": "extended_freeze",
+      "check": "baseline_isolation:income_tax:2032",
+      "expected": 3372.39990234375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 3318.39990234375,
+      "case_id": "extended_freeze",
+      "check": "baseline_isolation:income_tax:2033",
+      "expected": 3318.39990234375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 12830.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031",
+      "expected": 12830.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 38500.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[1].threshold:2031",
+      "expected": 38500.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 12830.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2031",
+      "expected": 12830.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 51330.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2031",
+      "expected": 51330.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 246.73,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031",
+      "expected": 246.73,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 987.12,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2031",
+      "expected": 987.12,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 3424.39990234375,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:income_tax:2031",
+      "expected": 3424.39990234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 13090.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.allowances.personal_allowance.amount:2032",
+      "expected": 13090.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 39300.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[1].threshold:2032",
+      "expected": 39300.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 13090.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2032",
+      "expected": 13090.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 52390.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2032",
+      "expected": 52390.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 251.73,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2032",
+      "expected": 251.73,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 1007.5,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2032",
+      "expected": 1007.5,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 3372.39990234375,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:income_tax:2032",
+      "expected": 3372.39990234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 13360.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.allowances.personal_allowance.amount:2033",
+      "expected": 13360.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 40100.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[1].threshold:2033",
+      "expected": 40100.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 13360.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2033",
+      "expected": 13360.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 53460.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2033",
+      "expected": 53460.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 256.92,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2033",
+      "expected": 256.92,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 1028.08,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2033",
+      "expected": 1028.08,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 3318.39990234375,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:income_tax:2033",
+      "expected": 3318.39990234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 13630.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034",
+      "expected": 13630.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 41000.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[1].threshold:2034",
+      "expected": 41000.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "83e1d809ac3a20b0669fcf3bf857cd0640aadd1be0b7713ea248fd96a7d38326",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 12570.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031:0",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031",
+      "decision_id": null,
+      "expected": 12570.0,
+      "holds": null,
+      "oracle": "frozen",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 37700.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031:1",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031",
+      "decision_id": null,
+      "expected": 37700.0,
+      "holds": null,
+      "oracle": "frozen",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12570.0,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2031:2",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2031",
+      "decision_id": null,
+      "expected": 12570.0,
+      "holds": null,
+      "oracle": "aligned with the allowance",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 50270.0,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2031:3",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2031",
+      "decision_id": null,
+      "expected": 50270.0,
+      "holds": null,
+      "oracle": "allowance + basic rate limit",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 241.73,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031:4",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031",
+      "decision_id": null,
+      "expected": 241.73,
+      "holds": null,
+      "oracle": "12,570 / 52, to the penny",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 966.73,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2031:5",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2031",
+      "decision_id": null,
+      "expected": 966.73,
+      "holds": null,
+      "oracle": "50,270 / 52, to the penny",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 52.0,
+      "assertion_id": "change:income_tax:2031:6",
+      "case_id": "extended_freeze",
+      "check": "change:income_tax:2031",
+      "decision_id": null,
+      "expected": 52.0,
+      "holds": null,
+      "oracle": "basic rate x (current law's allowance - 12,570)",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12570.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2032:7",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2032",
+      "decision_id": null,
+      "expected": 12570.0,
+      "holds": null,
+      "oracle": "frozen",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 37700.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2032:8",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2032",
+      "decision_id": null,
+      "expected": 37700.0,
+      "holds": null,
+      "oracle": "frozen",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12570.0,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2032:9",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2032",
+      "decision_id": null,
+      "expected": 12570.0,
+      "holds": null,
+      "oracle": "aligned with the allowance",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 50270.0,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2032:10",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2032",
+      "decision_id": null,
+      "expected": 50270.0,
+      "holds": null,
+      "oracle": "allowance + basic rate limit",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 241.73,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2032:11",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2032",
+      "decision_id": null,
+      "expected": 241.73,
+      "holds": null,
+      "oracle": "12,570 / 52, to the penny",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 966.73,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2032:12",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2032",
+      "decision_id": null,
+      "expected": 966.73,
+      "holds": null,
+      "oracle": "50,270 / 52, to the penny",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 104.0,
+      "assertion_id": "change:income_tax:2032:13",
+      "case_id": "extended_freeze",
+      "check": "change:income_tax:2032",
+      "decision_id": null,
+      "expected": 104.0,
+      "holds": null,
+      "oracle": "basic rate x (current law's allowance - 12,570)",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12570.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2033:14",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2033",
+      "decision_id": null,
+      "expected": 12570.0,
+      "holds": null,
+      "oracle": "frozen",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 37700.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2033:15",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2033",
+      "decision_id": null,
+      "expected": 37700.0,
+      "holds": null,
+      "oracle": "frozen",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12570.0,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2033:16",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2033",
+      "decision_id": null,
+      "expected": 12570.0,
+      "holds": null,
+      "oracle": "aligned with the allowance",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 50270.0,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2033:17",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2033",
+      "decision_id": null,
+      "expected": 50270.0,
+      "holds": null,
+      "oracle": "allowance + basic rate limit",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 241.73,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2033:18",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2033",
+      "decision_id": null,
+      "expected": 241.73,
+      "holds": null,
+      "oracle": "12,570 / 52, to the penny",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 966.73,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2033:19",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2033",
+      "decision_id": null,
+      "expected": 966.73,
+      "holds": null,
+      "oracle": "50,270 / 52, to the penny",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 158.0,
+      "assertion_id": "change:income_tax:2033:20",
+      "case_id": "extended_freeze",
+      "check": "change:income_tax:2033",
+      "decision_id": null,
+      "expected": 158.0,
+      "holds": null,
+      "oracle": "basic rate x (current law's allowance - 12,570)",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13630.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034:21",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034",
+      "decision_id": null,
+      "expected": 13630.0,
+      "holds": null,
+      "oracle": "prior law's projected path resumes (PRIOR_LAW_OPTION)",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13630.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034:22",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034",
+      "decision_id": "d1344",
+      "expected": 12830.0,
+      "holds": false,
+      "oracle": "ITA 2007 s57 from the frozen \u00a312,570 with 2034's September CPI rise",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pending"
+    },
+    {
+      "actual": 41000.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2034:23",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2034",
+      "decision_id": "d1344",
+      "expected": 38500.0,
+      "holds": false,
+      "oracle": "ITA 2007 s21 from the frozen \u00a337,700 with 2034's September CPI rise",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pending"
+    },
+    {
+      "actual": 3424.39990234375,
+      "case_id": "extended_freeze",
+      "check": "baseline_isolation:income_tax:2031",
+      "expected": 3424.39990234375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3372.39990234375,
+      "case_id": "extended_freeze",
+      "check": "baseline_isolation:income_tax:2032",
+      "expected": 3372.39990234375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3318.39990234375,
+      "case_id": "extended_freeze",
+      "check": "baseline_isolation:income_tax:2033",
+      "expected": 3318.39990234375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12830.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031",
+      "expected": 12830.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 38500.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[1].threshold:2031",
+      "expected": 38500.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12830.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2031",
+      "expected": 12830.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 51330.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2031",
+      "expected": 51330.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 246.73,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031",
+      "expected": 246.73,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 987.12,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2031",
+      "expected": 987.12,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3424.39990234375,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:income_tax:2031",
+      "expected": 3424.39990234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13090.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.allowances.personal_allowance.amount:2032",
+      "expected": 13090.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 39300.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[1].threshold:2032",
+      "expected": 39300.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13090.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2032",
+      "expected": 13090.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 52390.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2032",
+      "expected": 52390.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 251.73,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2032",
+      "expected": 251.73,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1007.5,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2032",
+      "expected": 1007.5,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3372.39990234375,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:income_tax:2032",
+      "expected": 3372.39990234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13360.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.allowances.personal_allowance.amount:2033",
+      "expected": 13360.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 40100.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[1].threshold:2033",
+      "expected": 40100.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13360.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2033",
+      "expected": 13360.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 53460.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2033",
+      "expected": 53460.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 256.92,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2033",
+      "expected": 256.92,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1028.08,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2033",
+      "expected": 1028.08,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 3318.39990234375,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:income_tax:2033",
+      "expected": 3318.39990234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13630.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034",
+      "expected": 13630.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 41000.0,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[1].threshold:2034",
+      "expected": 41000.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031:0",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031:1",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2031:2",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2031",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2031:3",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2031",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031:4",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2031:5",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2031",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "actual": 52.0,
+      "assertion_id": "change:income_tax:2031:6",
+      "case_id": "extended_freeze",
+      "check": "change:income_tax:2031",
+      "decision_id": null,
+      "expected": 52.0,
+      "holds": null,
+      "oracle": "basic rate x (current law's allowance - 12,570)",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2032:7",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2032",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2032:8",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2032",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2032:9",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2032",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2032:10",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2032",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2032:11",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2032",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2032:12",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2032",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "actual": 104.0,
+      "assertion_id": "change:income_tax:2032:13",
+      "case_id": "extended_freeze",
+      "check": "change:income_tax:2032",
+      "decision_id": null,
+      "expected": 104.0,
+      "holds": null,
+      "oracle": "basic rate x (current law's allowance - 12,570)",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2033:14",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2033",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2033:15",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2033",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2033:16",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2033",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2033:17",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2033",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2033:18",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2033",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2033:19",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2033",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "actual": 158.0,
+      "assertion_id": "change:income_tax:2033:20",
+      "case_id": "extended_freeze",
+      "check": "change:income_tax:2033",
+      "decision_id": null,
+      "expected": 158.0,
+      "holds": null,
+      "oracle": "basic rate x (current law's allowance - 12,570)",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034:21",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034:22",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2034:23",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2034",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "actual": 3424.39990234375,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:income_tax:2031",
+      "expected": 3424.39990234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 3372.39990234375,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:income_tax:2032",
+      "expected": 3372.39990234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 3318.39990234375,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:income_tax:2033",
+      "expected": 3318.39990234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031:0",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031:1",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2031:2",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2031",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2031:3",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2031",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031:4",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2031:5",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2031",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "actual": 52.0,
+      "assertion_id": "change:income_tax:2031:6",
+      "case_id": "extended_freeze",
+      "check": "change:income_tax:2031",
+      "decision_id": null,
+      "expected": 52.0,
+      "holds": null,
+      "oracle": "basic rate x (current law's allowance - 12,570)",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2032:7",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2032",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2032:8",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2032",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2032:9",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2032",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2032:10",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2032",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2032:11",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2032",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2032:12",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2032",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "actual": 104.0,
+      "assertion_id": "change:income_tax:2032:13",
+      "case_id": "extended_freeze",
+      "check": "change:income_tax:2032",
+      "decision_id": null,
+      "expected": 104.0,
+      "holds": null,
+      "oracle": "basic rate x (current law's allowance - 12,570)",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2033:14",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2033",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2033:15",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2033",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2033:16",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2033",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2033:17",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2033",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2033:18",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2033",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2033:19",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2033",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "actual": 158.0,
+      "assertion_id": "change:income_tax:2033:20",
+      "case_id": "extended_freeze",
+      "check": "change:income_tax:2033",
+      "decision_id": null,
+      "expected": 158.0,
+      "holds": null,
+      "oracle": "basic rate x (current law's allowance - 12,570)",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034:21",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034:22",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2034:23",
+      "case_id": "extended_freeze",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2034",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "actual": 3424.39990234375,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:income_tax:2031",
+      "expected": 3424.39990234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 3372.39990234375,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:income_tax:2032",
+      "expected": 3372.39990234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 3318.39990234375,
+      "case_id": "extended_freeze",
+      "check": "unreformed_after_reform:income_tax:2033",
+      "expected": 3318.39990234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "extended_freeze",
+        "reform_sha256": "8bcc44861af52ebd02b59342635d00e9900379ee1901ac755ea7443cb09d3d7c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "case_id": "extended_freeze",
+      "check": null,
+      "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+      "reform_digest": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "the route does not run in-process",
+      "route": "api.sim_api",
+      "status": "unsupported"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:uc_standard_allowance:2027:0",
+      "case_id": "april_benefit_uprating",
+      "check": "variable:uc_standard_allowance:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "\u00a3500 x 12",
+      "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "88407c594dab2d57ead4d2a0f358716dfef6f595741e7abbb206bc51bed87701",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 6240.0,
+      "assertion_id": "variable:uc_standard_allowance:2028:1",
+      "case_id": "april_benefit_uprating",
+      "check": "variable:uc_standard_allowance:2028",
+      "decision_id": null,
+      "expected": 6240.0,
+      "holds": null,
+      "oracle": "\u00a3520 x 12",
+      "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "88407c594dab2d57ead4d2a0f358716dfef6f595741e7abbb206bc51bed87701",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 6240.0,
+      "assertion_id": "variable:uc_standard_allowance:2029:2",
+      "case_id": "april_benefit_uprating",
+      "check": "variable:uc_standard_allowance:2029",
+      "decision_id": null,
+      "expected": 6240.0,
+      "holds": null,
+      "oracle": "held from April 2028",
+      "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "88407c594dab2d57ead4d2a0f358716dfef6f595741e7abbb206bc51bed87701",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:uc_standard_allowance:2026:3",
+      "case_id": "april_benefit_uprating",
+      "check": "change:uc_standard_allowance:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "88407c594dab2d57ead4d2a0f358716dfef6f595741e7abbb206bc51bed87701",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "april_benefit_uprating",
+      "check": "baseline_isolation:uc_standard_allowance:2027",
+      "expected": 5216.06103515625,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "88407c594dab2d57ead4d2a0f358716dfef6f595741e7abbb206bc51bed87701",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "april_benefit_uprating",
+      "check": "baseline_isolation:uc_standard_allowance:2028",
+      "expected": 5320.3740234375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "88407c594dab2d57ead4d2a0f358716dfef6f595741e7abbb206bc51bed87701",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5426.7763671875,
+      "case_id": "april_benefit_uprating",
+      "check": "baseline_isolation:uc_standard_allowance:2029",
+      "expected": 5426.7763671875,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "88407c594dab2d57ead4d2a0f358716dfef6f595741e7abbb206bc51bed87701",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5098.7998046875,
+      "case_id": "april_benefit_uprating",
+      "check": "baseline_isolation:uc_standard_allowance:2026",
+      "expected": 5098.7998046875,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "88407c594dab2d57ead4d2a0f358716dfef6f595741e7abbb206bc51bed87701",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2027",
+      "expected": 5216.06103515625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "88407c594dab2d57ead4d2a0f358716dfef6f595741e7abbb206bc51bed87701",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2028",
+      "expected": 5320.3740234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "88407c594dab2d57ead4d2a0f358716dfef6f595741e7abbb206bc51bed87701",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5426.7763671875,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2029",
+      "expected": 5426.7763671875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "88407c594dab2d57ead4d2a0f358716dfef6f595741e7abbb206bc51bed87701",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5098.7998046875,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2026",
+      "expected": 5098.7998046875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "88407c594dab2d57ead4d2a0f358716dfef6f595741e7abbb206bc51bed87701",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:uc_standard_allowance:2027:0",
+      "case_id": "april_benefit_uprating",
+      "check": "variable:uc_standard_allowance:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "\u00a3500 x 12",
+      "reform_digest": "0ab5df46a43311bce2af6c4b9b70155c64df30388c8a0f99f15a2846351f715a",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "40b2c4441a2078ee36efae8485cf7f09a44d30a419fe1a728ca638c0453e9d47",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 6240.0,
+      "assertion_id": "variable:uc_standard_allowance:2028:1",
+      "case_id": "april_benefit_uprating",
+      "check": "variable:uc_standard_allowance:2028",
+      "decision_id": null,
+      "expected": 6240.0,
+      "holds": null,
+      "oracle": "\u00a3520 x 12",
+      "reform_digest": "0ab5df46a43311bce2af6c4b9b70155c64df30388c8a0f99f15a2846351f715a",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "40b2c4441a2078ee36efae8485cf7f09a44d30a419fe1a728ca638c0453e9d47",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 6240.0,
+      "assertion_id": "variable:uc_standard_allowance:2029:2",
+      "case_id": "april_benefit_uprating",
+      "check": "variable:uc_standard_allowance:2029",
+      "decision_id": null,
+      "expected": 6240.0,
+      "holds": null,
+      "oracle": "held from April 2028",
+      "reform_digest": "0ab5df46a43311bce2af6c4b9b70155c64df30388c8a0f99f15a2846351f715a",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "40b2c4441a2078ee36efae8485cf7f09a44d30a419fe1a728ca638c0453e9d47",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:uc_standard_allowance:2026:3",
+      "case_id": "april_benefit_uprating",
+      "check": "change:uc_standard_allowance:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "0ab5df46a43311bce2af6c4b9b70155c64df30388c8a0f99f15a2846351f715a",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "40b2c4441a2078ee36efae8485cf7f09a44d30a419fe1a728ca638c0453e9d47",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "april_benefit_uprating",
+      "check": "baseline_isolation:uc_standard_allowance:2027",
+      "expected": 5216.06103515625,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "0ab5df46a43311bce2af6c4b9b70155c64df30388c8a0f99f15a2846351f715a",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "40b2c4441a2078ee36efae8485cf7f09a44d30a419fe1a728ca638c0453e9d47",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "april_benefit_uprating",
+      "check": "baseline_isolation:uc_standard_allowance:2028",
+      "expected": 5320.3740234375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "0ab5df46a43311bce2af6c4b9b70155c64df30388c8a0f99f15a2846351f715a",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "40b2c4441a2078ee36efae8485cf7f09a44d30a419fe1a728ca638c0453e9d47",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5426.7763671875,
+      "case_id": "april_benefit_uprating",
+      "check": "baseline_isolation:uc_standard_allowance:2029",
+      "expected": 5426.7763671875,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "0ab5df46a43311bce2af6c4b9b70155c64df30388c8a0f99f15a2846351f715a",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "40b2c4441a2078ee36efae8485cf7f09a44d30a419fe1a728ca638c0453e9d47",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5098.7998046875,
+      "case_id": "april_benefit_uprating",
+      "check": "baseline_isolation:uc_standard_allowance:2026",
+      "expected": 5098.7998046875,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "0ab5df46a43311bce2af6c4b9b70155c64df30388c8a0f99f15a2846351f715a",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "40b2c4441a2078ee36efae8485cf7f09a44d30a419fe1a728ca638c0453e9d47",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2027",
+      "expected": 5216.06103515625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "0ab5df46a43311bce2af6c4b9b70155c64df30388c8a0f99f15a2846351f715a",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "40b2c4441a2078ee36efae8485cf7f09a44d30a419fe1a728ca638c0453e9d47",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2028",
+      "expected": 5320.3740234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "0ab5df46a43311bce2af6c4b9b70155c64df30388c8a0f99f15a2846351f715a",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "40b2c4441a2078ee36efae8485cf7f09a44d30a419fe1a728ca638c0453e9d47",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5426.7763671875,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2029",
+      "expected": 5426.7763671875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "0ab5df46a43311bce2af6c4b9b70155c64df30388c8a0f99f15a2846351f715a",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "40b2c4441a2078ee36efae8485cf7f09a44d30a419fe1a728ca638c0453e9d47",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5098.7998046875,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2026",
+      "expected": 5098.7998046875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "0ab5df46a43311bce2af6c4b9b70155c64df30388c8a0f99f15a2846351f715a",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "40b2c4441a2078ee36efae8485cf7f09a44d30a419fe1a728ca638c0453e9d47",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:uc_standard_allowance:2027:0",
+      "case_id": "april_benefit_uprating",
+      "check": "variable:uc_standard_allowance:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "\u00a3500 x 12",
+      "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 6240.0,
+      "assertion_id": "variable:uc_standard_allowance:2028:1",
+      "case_id": "april_benefit_uprating",
+      "check": "variable:uc_standard_allowance:2028",
+      "decision_id": null,
+      "expected": 6240.0,
+      "holds": null,
+      "oracle": "\u00a3520 x 12",
+      "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 6240.0,
+      "assertion_id": "variable:uc_standard_allowance:2029:2",
+      "case_id": "april_benefit_uprating",
+      "check": "variable:uc_standard_allowance:2029",
+      "decision_id": null,
+      "expected": 6240.0,
+      "holds": null,
+      "oracle": "held from April 2028",
+      "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:uc_standard_allowance:2026:3",
+      "case_id": "april_benefit_uprating",
+      "check": "change:uc_standard_allowance:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "april_benefit_uprating",
+      "check": "baseline_isolation:uc_standard_allowance:2027",
+      "expected": 5216.06103515625,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "april_benefit_uprating",
+      "check": "baseline_isolation:uc_standard_allowance:2028",
+      "expected": 5320.3740234375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5426.7763671875,
+      "case_id": "april_benefit_uprating",
+      "check": "baseline_isolation:uc_standard_allowance:2029",
+      "expected": 5426.7763671875,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5098.7998046875,
+      "case_id": "april_benefit_uprating",
+      "check": "baseline_isolation:uc_standard_allowance:2026",
+      "expected": 5098.7998046875,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2027",
+      "expected": 5216.06103515625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2028",
+      "expected": 5320.3740234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5426.7763671875,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2029",
+      "expected": 5426.7763671875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5098.7998046875,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2026",
+      "expected": 5098.7998046875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:uc_standard_allowance:2027:0",
+      "case_id": "april_benefit_uprating",
+      "check": "variable:uc_standard_allowance:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "\u00a3500 x 12",
+      "reform_digest": "dfc339899202506f1bb7300cea49e605d3c128f1c036c0ec5df0c3d326d0c33e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "58e9d16e27a4dc0684cf02ecd1faafe3130e0ef7580678b9828ba90050e8b5b4",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 6240.0,
+      "assertion_id": "variable:uc_standard_allowance:2028:1",
+      "case_id": "april_benefit_uprating",
+      "check": "variable:uc_standard_allowance:2028",
+      "decision_id": null,
+      "expected": 6240.0,
+      "holds": null,
+      "oracle": "\u00a3520 x 12",
+      "reform_digest": "dfc339899202506f1bb7300cea49e605d3c128f1c036c0ec5df0c3d326d0c33e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "58e9d16e27a4dc0684cf02ecd1faafe3130e0ef7580678b9828ba90050e8b5b4",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 6240.0,
+      "assertion_id": "variable:uc_standard_allowance:2029:2",
+      "case_id": "april_benefit_uprating",
+      "check": "variable:uc_standard_allowance:2029",
+      "decision_id": null,
+      "expected": 6240.0,
+      "holds": null,
+      "oracle": "held from April 2028",
+      "reform_digest": "dfc339899202506f1bb7300cea49e605d3c128f1c036c0ec5df0c3d326d0c33e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "58e9d16e27a4dc0684cf02ecd1faafe3130e0ef7580678b9828ba90050e8b5b4",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:uc_standard_allowance:2026:3",
+      "case_id": "april_benefit_uprating",
+      "check": "change:uc_standard_allowance:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "dfc339899202506f1bb7300cea49e605d3c128f1c036c0ec5df0c3d326d0c33e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "58e9d16e27a4dc0684cf02ecd1faafe3130e0ef7580678b9828ba90050e8b5b4",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2027",
+      "expected": 5216.06103515625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "dfc339899202506f1bb7300cea49e605d3c128f1c036c0ec5df0c3d326d0c33e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "58e9d16e27a4dc0684cf02ecd1faafe3130e0ef7580678b9828ba90050e8b5b4",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2028",
+      "expected": 5320.3740234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "dfc339899202506f1bb7300cea49e605d3c128f1c036c0ec5df0c3d326d0c33e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "58e9d16e27a4dc0684cf02ecd1faafe3130e0ef7580678b9828ba90050e8b5b4",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 5426.7763671875,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2029",
+      "expected": 5426.7763671875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "dfc339899202506f1bb7300cea49e605d3c128f1c036c0ec5df0c3d326d0c33e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "58e9d16e27a4dc0684cf02ecd1faafe3130e0ef7580678b9828ba90050e8b5b4",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 5098.7998046875,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2026",
+      "expected": 5098.7998046875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "dfc339899202506f1bb7300cea49e605d3c128f1c036c0ec5df0c3d326d0c33e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "58e9d16e27a4dc0684cf02ecd1faafe3130e0ef7580678b9828ba90050e8b5b4",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:uc_standard_allowance:2027:0",
+      "case_id": "april_benefit_uprating",
+      "check": "variable:uc_standard_allowance:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "\u00a3500 x 12",
+      "reform_digest": "d4388eb374c51a6da454f1325fe7699aca1a9004b95dae95464c3a9b2aea8bd5",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "58e9d16e27a4dc0684cf02ecd1faafe3130e0ef7580678b9828ba90050e8b5b4",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 6240.0,
+      "assertion_id": "variable:uc_standard_allowance:2028:1",
+      "case_id": "april_benefit_uprating",
+      "check": "variable:uc_standard_allowance:2028",
+      "decision_id": null,
+      "expected": 6240.0,
+      "holds": null,
+      "oracle": "\u00a3520 x 12",
+      "reform_digest": "d4388eb374c51a6da454f1325fe7699aca1a9004b95dae95464c3a9b2aea8bd5",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "58e9d16e27a4dc0684cf02ecd1faafe3130e0ef7580678b9828ba90050e8b5b4",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 6240.0,
+      "assertion_id": "variable:uc_standard_allowance:2029:2",
+      "case_id": "april_benefit_uprating",
+      "check": "variable:uc_standard_allowance:2029",
+      "decision_id": null,
+      "expected": 6240.0,
+      "holds": null,
+      "oracle": "held from April 2028",
+      "reform_digest": "d4388eb374c51a6da454f1325fe7699aca1a9004b95dae95464c3a9b2aea8bd5",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "58e9d16e27a4dc0684cf02ecd1faafe3130e0ef7580678b9828ba90050e8b5b4",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:uc_standard_allowance:2026:3",
+      "case_id": "april_benefit_uprating",
+      "check": "change:uc_standard_allowance:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "d4388eb374c51a6da454f1325fe7699aca1a9004b95dae95464c3a9b2aea8bd5",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "58e9d16e27a4dc0684cf02ecd1faafe3130e0ef7580678b9828ba90050e8b5b4",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2027",
+      "expected": 5216.06103515625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "d4388eb374c51a6da454f1325fe7699aca1a9004b95dae95464c3a9b2aea8bd5",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "58e9d16e27a4dc0684cf02ecd1faafe3130e0ef7580678b9828ba90050e8b5b4",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2028",
+      "expected": 5320.3740234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "d4388eb374c51a6da454f1325fe7699aca1a9004b95dae95464c3a9b2aea8bd5",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "58e9d16e27a4dc0684cf02ecd1faafe3130e0ef7580678b9828ba90050e8b5b4",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 5426.7763671875,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2029",
+      "expected": 5426.7763671875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "d4388eb374c51a6da454f1325fe7699aca1a9004b95dae95464c3a9b2aea8bd5",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "58e9d16e27a4dc0684cf02ecd1faafe3130e0ef7580678b9828ba90050e8b5b4",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 5098.7998046875,
+      "case_id": "april_benefit_uprating",
+      "check": "unreformed_after_reform:uc_standard_allowance:2026",
+      "expected": 5098.7998046875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "d4388eb374c51a6da454f1325fe7699aca1a9004b95dae95464c3a9b2aea8bd5",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "april_benefit_uprating",
+        "reform_sha256": "58e9d16e27a4dc0684cf02ecd1faafe3130e0ef7580678b9828ba90050e8b5b4",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "case_id": "april_benefit_uprating",
+      "check": null,
+      "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+      "reform_digest": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "the route does not run in-process",
+      "route": "api.sim_api",
+      "status": "unsupported"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:uc_standard_allowance:2027:0",
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "variable:uc_standard_allowance:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "\u00a3500 x 12",
+      "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "db26dec4b5e242418423a73dd50b1e505234e3cabc766b7a69c6b090861fc222",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:uc_standard_allowance:2028:1",
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "change:uc_standard_allowance:2028",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "prior law's uprated path resumes",
+      "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "db26dec4b5e242418423a73dd50b1e505234e3cabc766b7a69c6b090861fc222",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:uc_standard_allowance:2030:2",
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "change:uc_standard_allowance:2030",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "prior law's uprated path resumes",
+      "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "db26dec4b5e242418423a73dd50b1e505234e3cabc766b7a69c6b090861fc222",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "baseline_isolation:uc_standard_allowance:2027",
+      "expected": 5216.06103515625,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "db26dec4b5e242418423a73dd50b1e505234e3cabc766b7a69c6b090861fc222",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "baseline_isolation:uc_standard_allowance:2028",
+      "expected": 5320.3740234375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "db26dec4b5e242418423a73dd50b1e505234e3cabc766b7a69c6b090861fc222",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5535.2998046875,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "baseline_isolation:uc_standard_allowance:2030",
+      "expected": 5535.2998046875,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "db26dec4b5e242418423a73dd50b1e505234e3cabc766b7a69c6b090861fc222",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "unreformed_after_reform:uc_standard_allowance:2027",
+      "expected": 5216.06103515625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "db26dec4b5e242418423a73dd50b1e505234e3cabc766b7a69c6b090861fc222",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "unreformed_after_reform:uc_standard_allowance:2028",
+      "expected": 5320.3740234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "db26dec4b5e242418423a73dd50b1e505234e3cabc766b7a69c6b090861fc222",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5535.2998046875,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "unreformed_after_reform:uc_standard_allowance:2030",
+      "expected": 5535.2998046875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "db26dec4b5e242418423a73dd50b1e505234e3cabc766b7a69c6b090861fc222",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:uc_standard_allowance:2027:0",
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "variable:uc_standard_allowance:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "\u00a3500 x 12",
+      "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "51108be17b7300ba327842351ba352d0348dc28541775d7ebb8f6996b163b058",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:uc_standard_allowance:2028:1",
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "change:uc_standard_allowance:2028",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "prior law's uprated path resumes",
+      "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "51108be17b7300ba327842351ba352d0348dc28541775d7ebb8f6996b163b058",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:uc_standard_allowance:2030:2",
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "change:uc_standard_allowance:2030",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "prior law's uprated path resumes",
+      "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "51108be17b7300ba327842351ba352d0348dc28541775d7ebb8f6996b163b058",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "baseline_isolation:uc_standard_allowance:2027",
+      "expected": 5216.06103515625,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "51108be17b7300ba327842351ba352d0348dc28541775d7ebb8f6996b163b058",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "baseline_isolation:uc_standard_allowance:2028",
+      "expected": 5320.3740234375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "51108be17b7300ba327842351ba352d0348dc28541775d7ebb8f6996b163b058",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5535.2998046875,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "baseline_isolation:uc_standard_allowance:2030",
+      "expected": 5535.2998046875,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "51108be17b7300ba327842351ba352d0348dc28541775d7ebb8f6996b163b058",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "unreformed_after_reform:uc_standard_allowance:2027",
+      "expected": 5216.06103515625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "51108be17b7300ba327842351ba352d0348dc28541775d7ebb8f6996b163b058",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "unreformed_after_reform:uc_standard_allowance:2028",
+      "expected": 5320.3740234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "51108be17b7300ba327842351ba352d0348dc28541775d7ebb8f6996b163b058",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5535.2998046875,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "unreformed_after_reform:uc_standard_allowance:2030",
+      "expected": 5535.2998046875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "51108be17b7300ba327842351ba352d0348dc28541775d7ebb8f6996b163b058",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:uc_standard_allowance:2027:0",
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "variable:uc_standard_allowance:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "\u00a3500 x 12",
+      "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:uc_standard_allowance:2028:1",
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "change:uc_standard_allowance:2028",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "prior law's uprated path resumes",
+      "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:uc_standard_allowance:2030:2",
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "change:uc_standard_allowance:2030",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "prior law's uprated path resumes",
+      "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "baseline_isolation:uc_standard_allowance:2027",
+      "expected": 5216.06103515625,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "baseline_isolation:uc_standard_allowance:2028",
+      "expected": 5320.3740234375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5535.2998046875,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "baseline_isolation:uc_standard_allowance:2030",
+      "expected": 5535.2998046875,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "unreformed_after_reform:uc_standard_allowance:2027",
+      "expected": 5216.06103515625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "unreformed_after_reform:uc_standard_allowance:2028",
+      "expected": 5320.3740234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5535.2998046875,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "unreformed_after_reform:uc_standard_allowance:2030",
+      "expected": 5535.2998046875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:uc_standard_allowance:2027:0",
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "variable:uc_standard_allowance:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "\u00a3500 x 12",
+      "reform_digest": "6df62a9f41db002ef95cadf25f2cd566de071a424141ebd1c479e4398ead4a4e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:uc_standard_allowance:2028:1",
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "change:uc_standard_allowance:2028",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "prior law's uprated path resumes",
+      "reform_digest": "6df62a9f41db002ef95cadf25f2cd566de071a424141ebd1c479e4398ead4a4e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:uc_standard_allowance:2030:2",
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "change:uc_standard_allowance:2030",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "prior law's uprated path resumes",
+      "reform_digest": "6df62a9f41db002ef95cadf25f2cd566de071a424141ebd1c479e4398ead4a4e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "unreformed_after_reform:uc_standard_allowance:2027",
+      "expected": 5216.06103515625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "6df62a9f41db002ef95cadf25f2cd566de071a424141ebd1c479e4398ead4a4e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "unreformed_after_reform:uc_standard_allowance:2028",
+      "expected": 5320.3740234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "6df62a9f41db002ef95cadf25f2cd566de071a424141ebd1c479e4398ead4a4e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 5535.2998046875,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "unreformed_after_reform:uc_standard_allowance:2030",
+      "expected": 5535.2998046875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "6df62a9f41db002ef95cadf25f2cd566de071a424141ebd1c479e4398ead4a4e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:uc_standard_allowance:2027:0",
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "variable:uc_standard_allowance:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "\u00a3500 x 12",
+      "reform_digest": "48f71ddba48e9494e868325cf1af191218d7edb5c4846616dbe75500e6db417d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:uc_standard_allowance:2028:1",
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "change:uc_standard_allowance:2028",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "prior law's uprated path resumes",
+      "reform_digest": "48f71ddba48e9494e868325cf1af191218d7edb5c4846616dbe75500e6db417d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:uc_standard_allowance:2030:2",
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "change:uc_standard_allowance:2030",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "prior law's uprated path resumes",
+      "reform_digest": "48f71ddba48e9494e868325cf1af191218d7edb5c4846616dbe75500e6db417d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "unreformed_after_reform:uc_standard_allowance:2027",
+      "expected": 5216.06103515625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "48f71ddba48e9494e868325cf1af191218d7edb5c4846616dbe75500e6db417d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "unreformed_after_reform:uc_standard_allowance:2028",
+      "expected": 5320.3740234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "48f71ddba48e9494e868325cf1af191218d7edb5c4846616dbe75500e6db417d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 5535.2998046875,
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": "unreformed_after_reform:uc_standard_allowance:2030",
+      "expected": 5535.2998046875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "48f71ddba48e9494e868325cf1af191218d7edb5c4846616dbe75500e6db417d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "bounded_change_on_uprated_benefit",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "case_id": "bounded_change_on_uprated_benefit",
+      "check": null,
+      "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+      "reform_digest": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "the route does not run in-process",
+      "route": "api.sim_api",
+      "status": "unsupported"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:universal_credit:2027:0",
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "variable:universal_credit:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "with no income or other elements, the award is the standard allowance: \u00a3500 x 12",
+      "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "db26dec4b5e242418423a73dd50b1e505234e3cabc766b7a69c6b090861fc222",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:universal_credit:2026:1",
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "change:universal_credit:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "db26dec4b5e242418423a73dd50b1e505234e3cabc766b7a69c6b090861fc222",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:universal_credit:2028:2",
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "change:universal_credit:2028",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "prior law's uprated path resumes",
+      "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "db26dec4b5e242418423a73dd50b1e505234e3cabc766b7a69c6b090861fc222",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "baseline_isolation:universal_credit:2027",
+      "expected": 5216.06103515625,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "db26dec4b5e242418423a73dd50b1e505234e3cabc766b7a69c6b090861fc222",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5098.7998046875,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "baseline_isolation:universal_credit:2026",
+      "expected": 5098.7998046875,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "db26dec4b5e242418423a73dd50b1e505234e3cabc766b7a69c6b090861fc222",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "baseline_isolation:universal_credit:2028",
+      "expected": 5320.3740234375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "db26dec4b5e242418423a73dd50b1e505234e3cabc766b7a69c6b090861fc222",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "unreformed_after_reform:universal_credit:2027",
+      "expected": 5216.06103515625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "db26dec4b5e242418423a73dd50b1e505234e3cabc766b7a69c6b090861fc222",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5098.7998046875,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "unreformed_after_reform:universal_credit:2026",
+      "expected": 5098.7998046875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "db26dec4b5e242418423a73dd50b1e505234e3cabc766b7a69c6b090861fc222",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "unreformed_after_reform:universal_credit:2028",
+      "expected": 5320.3740234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "db26dec4b5e242418423a73dd50b1e505234e3cabc766b7a69c6b090861fc222",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:universal_credit:2027:0",
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "variable:universal_credit:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "with no income or other elements, the award is the standard allowance: \u00a3500 x 12",
+      "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "51108be17b7300ba327842351ba352d0348dc28541775d7ebb8f6996b163b058",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:universal_credit:2026:1",
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "change:universal_credit:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "51108be17b7300ba327842351ba352d0348dc28541775d7ebb8f6996b163b058",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:universal_credit:2028:2",
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "change:universal_credit:2028",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "prior law's uprated path resumes",
+      "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "51108be17b7300ba327842351ba352d0348dc28541775d7ebb8f6996b163b058",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "baseline_isolation:universal_credit:2027",
+      "expected": 5216.06103515625,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "51108be17b7300ba327842351ba352d0348dc28541775d7ebb8f6996b163b058",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5098.7998046875,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "baseline_isolation:universal_credit:2026",
+      "expected": 5098.7998046875,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "51108be17b7300ba327842351ba352d0348dc28541775d7ebb8f6996b163b058",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "baseline_isolation:universal_credit:2028",
+      "expected": 5320.3740234375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "51108be17b7300ba327842351ba352d0348dc28541775d7ebb8f6996b163b058",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "unreformed_after_reform:universal_credit:2027",
+      "expected": 5216.06103515625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "51108be17b7300ba327842351ba352d0348dc28541775d7ebb8f6996b163b058",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5098.7998046875,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "unreformed_after_reform:universal_credit:2026",
+      "expected": 5098.7998046875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "51108be17b7300ba327842351ba352d0348dc28541775d7ebb8f6996b163b058",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "unreformed_after_reform:universal_credit:2028",
+      "expected": 5320.3740234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "51108be17b7300ba327842351ba352d0348dc28541775d7ebb8f6996b163b058",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:universal_credit:2027:0",
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "variable:universal_credit:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "with no income or other elements, the award is the standard allowance: \u00a3500 x 12",
+      "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:universal_credit:2026:1",
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "change:universal_credit:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:universal_credit:2028:2",
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "change:universal_credit:2028",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "prior law's uprated path resumes",
+      "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "baseline_isolation:universal_credit:2027",
+      "expected": 5216.06103515625,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5098.7998046875,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "baseline_isolation:universal_credit:2026",
+      "expected": 5098.7998046875,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "baseline_isolation:universal_credit:2028",
+      "expected": 5320.3740234375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "unreformed_after_reform:universal_credit:2027",
+      "expected": 5216.06103515625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5098.7998046875,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "unreformed_after_reform:universal_credit:2026",
+      "expected": 5098.7998046875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "unreformed_after_reform:universal_credit:2028",
+      "expected": 5320.3740234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:universal_credit:2027:0",
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "variable:universal_credit:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "with no income or other elements, the award is the standard allowance: \u00a3500 x 12",
+      "reform_digest": "6df62a9f41db002ef95cadf25f2cd566de071a424141ebd1c479e4398ead4a4e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:universal_credit:2026:1",
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "change:universal_credit:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "6df62a9f41db002ef95cadf25f2cd566de071a424141ebd1c479e4398ead4a4e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:universal_credit:2028:2",
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "change:universal_credit:2028",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "prior law's uprated path resumes",
+      "reform_digest": "6df62a9f41db002ef95cadf25f2cd566de071a424141ebd1c479e4398ead4a4e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "unreformed_after_reform:universal_credit:2027",
+      "expected": 5216.06103515625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "6df62a9f41db002ef95cadf25f2cd566de071a424141ebd1c479e4398ead4a4e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 5098.7998046875,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "unreformed_after_reform:universal_credit:2026",
+      "expected": 5098.7998046875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "6df62a9f41db002ef95cadf25f2cd566de071a424141ebd1c479e4398ead4a4e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "unreformed_after_reform:universal_credit:2028",
+      "expected": 5320.3740234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "6df62a9f41db002ef95cadf25f2cd566de071a424141ebd1c479e4398ead4a4e",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:universal_credit:2027:0",
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "variable:universal_credit:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "with no income or other elements, the award is the standard allowance: \u00a3500 x 12",
+      "reform_digest": "48f71ddba48e9494e868325cf1af191218d7edb5c4846616dbe75500e6db417d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:universal_credit:2026:1",
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "change:universal_credit:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "48f71ddba48e9494e868325cf1af191218d7edb5c4846616dbe75500e6db417d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:universal_credit:2028:2",
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "change:universal_credit:2028",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "prior law's uprated path resumes",
+      "reform_digest": "48f71ddba48e9494e868325cf1af191218d7edb5c4846616dbe75500e6db417d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "unreformed_after_reform:universal_credit:2027",
+      "expected": 5216.06103515625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "48f71ddba48e9494e868325cf1af191218d7edb5c4846616dbe75500e6db417d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 5098.7998046875,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "unreformed_after_reform:universal_credit:2026",
+      "expected": 5098.7998046875,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "48f71ddba48e9494e868325cf1af191218d7edb5c4846616dbe75500e6db417d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": "unreformed_after_reform:universal_credit:2028",
+      "expected": 5320.3740234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "48f71ddba48e9494e868325cf1af191218d7edb5c4846616dbe75500e6db417d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_award_follows_the_standard_allowance",
+        "reform_sha256": "1fab70cb70b7ee142be73c2e6b4b6e9056e2c2562393a351b226ee865bcaaf32",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "case_id": "uc_award_follows_the_standard_allowance",
+      "check": null,
+      "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+      "reform_digest": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "the route does not run in-process",
+      "route": "api.sim_api",
+      "status": "unsupported"
+    },
+    {
+      "actual": 14560.0,
+      "assertion_id": "variable:new_state_pension:2027:0",
+      "case_id": "new_state_pension_rate",
+      "check": "variable:new_state_pension:2027",
+      "decision_id": null,
+      "expected": 14560.0,
+      "holds": null,
+      "oracle": "the full weekly rate x 52: \u00a3280 x 52",
+      "reform_digest": "7c9f24257e0db73b283f97e34d0226bb7ef3deeb87e53cb9b6d8ae0813d8ffef",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "0945e3ed8790352bb34d002b0f6c9dbabc55112d4f836a05c72e546a3c445026",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:new_state_pension:2026:1",
+      "case_id": "new_state_pension_rate",
+      "check": "change:new_state_pension:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "7c9f24257e0db73b283f97e34d0226bb7ef3deeb87e53cb9b6d8ae0813d8ffef",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "0945e3ed8790352bb34d002b0f6c9dbabc55112d4f836a05c72e546a3c445026",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13036.9736328125,
+      "case_id": "new_state_pension_rate",
+      "check": "baseline_isolation:new_state_pension:2027",
+      "expected": 13036.9736328125,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "7c9f24257e0db73b283f97e34d0226bb7ef3deeb87e53cb9b6d8ae0813d8ffef",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "0945e3ed8790352bb34d002b0f6c9dbabc55112d4f836a05c72e546a3c445026",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12547.599609375,
+      "case_id": "new_state_pension_rate",
+      "check": "baseline_isolation:new_state_pension:2026",
+      "expected": 12547.599609375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "7c9f24257e0db73b283f97e34d0226bb7ef3deeb87e53cb9b6d8ae0813d8ffef",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "0945e3ed8790352bb34d002b0f6c9dbabc55112d4f836a05c72e546a3c445026",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13036.9736328125,
+      "case_id": "new_state_pension_rate",
+      "check": "unreformed_after_reform:new_state_pension:2027",
+      "expected": 13036.9736328125,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "7c9f24257e0db73b283f97e34d0226bb7ef3deeb87e53cb9b6d8ae0813d8ffef",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "0945e3ed8790352bb34d002b0f6c9dbabc55112d4f836a05c72e546a3c445026",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12547.599609375,
+      "case_id": "new_state_pension_rate",
+      "check": "unreformed_after_reform:new_state_pension:2026",
+      "expected": 12547.599609375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "7c9f24257e0db73b283f97e34d0226bb7ef3deeb87e53cb9b6d8ae0813d8ffef",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "0945e3ed8790352bb34d002b0f6c9dbabc55112d4f836a05c72e546a3c445026",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 14560.0,
+      "assertion_id": "variable:new_state_pension:2027:0",
+      "case_id": "new_state_pension_rate",
+      "check": "variable:new_state_pension:2027",
+      "decision_id": null,
+      "expected": 14560.0,
+      "holds": null,
+      "oracle": "the full weekly rate x 52: \u00a3280 x 52",
+      "reform_digest": "c3d7325ace7970b8e9ee72145bbd5355a951fb28634263fe8d45bf5b5bef852b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "0dcc940a6844540a4b247471f865123986aa0880e6db798bd4cdf9e1d39d7afa",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:new_state_pension:2026:1",
+      "case_id": "new_state_pension_rate",
+      "check": "change:new_state_pension:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "c3d7325ace7970b8e9ee72145bbd5355a951fb28634263fe8d45bf5b5bef852b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "0dcc940a6844540a4b247471f865123986aa0880e6db798bd4cdf9e1d39d7afa",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 13036.9736328125,
+      "case_id": "new_state_pension_rate",
+      "check": "baseline_isolation:new_state_pension:2027",
+      "expected": 13036.9736328125,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "c3d7325ace7970b8e9ee72145bbd5355a951fb28634263fe8d45bf5b5bef852b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "0dcc940a6844540a4b247471f865123986aa0880e6db798bd4cdf9e1d39d7afa",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 12547.599609375,
+      "case_id": "new_state_pension_rate",
+      "check": "baseline_isolation:new_state_pension:2026",
+      "expected": 12547.599609375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "c3d7325ace7970b8e9ee72145bbd5355a951fb28634263fe8d45bf5b5bef852b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "0dcc940a6844540a4b247471f865123986aa0880e6db798bd4cdf9e1d39d7afa",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 13036.9736328125,
+      "case_id": "new_state_pension_rate",
+      "check": "unreformed_after_reform:new_state_pension:2027",
+      "expected": 13036.9736328125,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "c3d7325ace7970b8e9ee72145bbd5355a951fb28634263fe8d45bf5b5bef852b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "0dcc940a6844540a4b247471f865123986aa0880e6db798bd4cdf9e1d39d7afa",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 12547.599609375,
+      "case_id": "new_state_pension_rate",
+      "check": "unreformed_after_reform:new_state_pension:2026",
+      "expected": 12547.599609375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "c3d7325ace7970b8e9ee72145bbd5355a951fb28634263fe8d45bf5b5bef852b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "0dcc940a6844540a4b247471f865123986aa0880e6db798bd4cdf9e1d39d7afa",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 14560.0,
+      "assertion_id": "variable:new_state_pension:2027:0",
+      "case_id": "new_state_pension_rate",
+      "check": "variable:new_state_pension:2027",
+      "decision_id": null,
+      "expected": 14560.0,
+      "holds": null,
+      "oracle": "the full weekly rate x 52: \u00a3280 x 52",
+      "reform_digest": "2d1c4aca40ef23e4e38ee649a9a1326147605eb994e0a45c159dc41f9bb6916e",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:new_state_pension:2026:1",
+      "case_id": "new_state_pension_rate",
+      "check": "change:new_state_pension:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "2d1c4aca40ef23e4e38ee649a9a1326147605eb994e0a45c159dc41f9bb6916e",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13036.9736328125,
+      "case_id": "new_state_pension_rate",
+      "check": "baseline_isolation:new_state_pension:2027",
+      "expected": 13036.9736328125,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "2d1c4aca40ef23e4e38ee649a9a1326147605eb994e0a45c159dc41f9bb6916e",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12547.599609375,
+      "case_id": "new_state_pension_rate",
+      "check": "baseline_isolation:new_state_pension:2026",
+      "expected": 12547.599609375,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "2d1c4aca40ef23e4e38ee649a9a1326147605eb994e0a45c159dc41f9bb6916e",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13036.9736328125,
+      "case_id": "new_state_pension_rate",
+      "check": "unreformed_after_reform:new_state_pension:2027",
+      "expected": 13036.9736328125,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "2d1c4aca40ef23e4e38ee649a9a1326147605eb994e0a45c159dc41f9bb6916e",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12547.599609375,
+      "case_id": "new_state_pension_rate",
+      "check": "unreformed_after_reform:new_state_pension:2026",
+      "expected": 12547.599609375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "2d1c4aca40ef23e4e38ee649a9a1326147605eb994e0a45c159dc41f9bb6916e",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 14560.0,
+      "assertion_id": "variable:new_state_pension:2027:0",
+      "case_id": "new_state_pension_rate",
+      "check": "variable:new_state_pension:2027",
+      "decision_id": null,
+      "expected": 14560.0,
+      "holds": null,
+      "oracle": "the full weekly rate x 52: \u00a3280 x 52",
+      "reform_digest": "0b2bf84c45612ec5d6e3801f4f9e65f4c60f4790c0fec009a23e2740fb01a88a",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "1adb71bb9501e5eac518c2a5052887d74f26bd50c5d31f80a7e4a93d48ebdf63",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:new_state_pension:2026:1",
+      "case_id": "new_state_pension_rate",
+      "check": "change:new_state_pension:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "0b2bf84c45612ec5d6e3801f4f9e65f4c60f4790c0fec009a23e2740fb01a88a",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "1adb71bb9501e5eac518c2a5052887d74f26bd50c5d31f80a7e4a93d48ebdf63",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 13036.9736328125,
+      "case_id": "new_state_pension_rate",
+      "check": "unreformed_after_reform:new_state_pension:2027",
+      "expected": 13036.9736328125,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "0b2bf84c45612ec5d6e3801f4f9e65f4c60f4790c0fec009a23e2740fb01a88a",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "1adb71bb9501e5eac518c2a5052887d74f26bd50c5d31f80a7e4a93d48ebdf63",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 12547.599609375,
+      "case_id": "new_state_pension_rate",
+      "check": "unreformed_after_reform:new_state_pension:2026",
+      "expected": 12547.599609375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "0b2bf84c45612ec5d6e3801f4f9e65f4c60f4790c0fec009a23e2740fb01a88a",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "1adb71bb9501e5eac518c2a5052887d74f26bd50c5d31f80a7e4a93d48ebdf63",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 14560.0,
+      "assertion_id": "variable:new_state_pension:2027:0",
+      "case_id": "new_state_pension_rate",
+      "check": "variable:new_state_pension:2027",
+      "decision_id": null,
+      "expected": 14560.0,
+      "holds": null,
+      "oracle": "the full weekly rate x 52: \u00a3280 x 52",
+      "reform_digest": "52299dd48f65858b74ac05e1fb951f0b751dd0a6ed3430393643797f95ef5fff",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "1adb71bb9501e5eac518c2a5052887d74f26bd50c5d31f80a7e4a93d48ebdf63",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:new_state_pension:2026:1",
+      "case_id": "new_state_pension_rate",
+      "check": "change:new_state_pension:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "52299dd48f65858b74ac05e1fb951f0b751dd0a6ed3430393643797f95ef5fff",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "1adb71bb9501e5eac518c2a5052887d74f26bd50c5d31f80a7e4a93d48ebdf63",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 13036.9736328125,
+      "case_id": "new_state_pension_rate",
+      "check": "unreformed_after_reform:new_state_pension:2027",
+      "expected": 13036.9736328125,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "52299dd48f65858b74ac05e1fb951f0b751dd0a6ed3430393643797f95ef5fff",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "1adb71bb9501e5eac518c2a5052887d74f26bd50c5d31f80a7e4a93d48ebdf63",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 12547.599609375,
+      "case_id": "new_state_pension_rate",
+      "check": "unreformed_after_reform:new_state_pension:2026",
+      "expected": 12547.599609375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "52299dd48f65858b74ac05e1fb951f0b751dd0a6ed3430393643797f95ef5fff",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "new_state_pension_rate",
+        "reform_sha256": "1adb71bb9501e5eac518c2a5052887d74f26bd50c5d31f80a7e4a93d48ebdf63",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "case_id": "new_state_pension_rate",
+      "check": null,
+      "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+      "reform_digest": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "the route does not run in-process",
+      "route": "api.sim_api",
+      "status": "unsupported"
+    },
+    {
+      "actual": 0.18868852459016394,
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2027:0",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2027",
+      "decision_id": null,
+      "expected": 0.18868852459016502,
+      "holds": null,
+      "oracle": "day-weighted: 207 days at 18% and 159 at 20% of 366",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2028:1",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2028",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "in force all year",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.18000000000000002,
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2026:2",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2026",
+      "decision_id": null,
+      "expected": 0.18,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.elasticity:2027:3",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.simulation.capital_gains_responses.elasticity:2027",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "set by the case",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027:4",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "set by the case",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027:5",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "set by the case",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:capital_gains_tax:2026:6",
+      "case_id": "mid_year_blended_rate",
+      "check": "change:capital_gains_tax:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 60.8197021484375,
+      "assertion_id": "change:capital_gains_tax:2027:7",
+      "case_id": "mid_year_blended_rate",
+      "check": "change:capital_gains_tax:2027",
+      "decision_id": null,
+      "expected": 60.81967213114759,
+      "holds": null,
+      "oracle": "(\u00a310,000 of gains - the annual exempt amount) x (20% - current rate) x the share of the tax year at 20%, with realisations held fixed; the gains sit wholly in the basic rate band, and the exempt amount and current rate are read from current law; 159 of 2027-28's 366 days",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 140.0,
+      "assertion_id": "change:capital_gains_tax:2028:8",
+      "case_id": "mid_year_blended_rate",
+      "check": "change:capital_gains_tax:2028",
+      "decision_id": null,
+      "expected": 139.99999999999994,
+      "holds": null,
+      "oracle": "(\u00a310,000 of gains - the annual exempt amount) x (20% - current rate) x the share of the tax year at 20%, with realisations held fixed; the gains sit wholly in the basic rate band, and the exempt amount and current rate are read from current law; all of 2028-29",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "baseline_isolation:capital_gains_tax:2026",
+      "expected": 1260.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "baseline_isolation:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "baseline_isolation:capital_gains_tax:2028",
+      "expected": 1260.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.18,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:gov.hmrc.cgt.basic_rate:2027",
+      "expected": 0.18,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.18000000000000002,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:gov.hmrc.cgt.basic_rate:2028",
+      "expected": 0.18000000000000002,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.18000000000000002,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:gov.hmrc.cgt.basic_rate:2026",
+      "expected": 0.18000000000000002,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:capital_gains_tax:2026",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:capital_gains_tax:2028",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fd206e406d990aac403379d58a58e04950c54aff179efbe8895d52f08fd8902b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "2f6ae5060f9aee2dca401328ce7681920741c0202fb7a5b4c86823c6f7ca5ea3",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.18868852459016394,
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2027:0",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2027",
+      "decision_id": null,
+      "expected": 0.18868852459016502,
+      "holds": null,
+      "oracle": "day-weighted: 207 days at 18% and 159 at 20% of 366",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2028:1",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2028",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "in force all year",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.18000000000000002,
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2026:2",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2026",
+      "decision_id": null,
+      "expected": 0.18,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.elasticity:2027:3",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.simulation.capital_gains_responses.elasticity:2027",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "set by the case",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027:4",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "set by the case",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027:5",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "set by the case",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:capital_gains_tax:2026:6",
+      "case_id": "mid_year_blended_rate",
+      "check": "change:capital_gains_tax:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 60.8197021484375,
+      "assertion_id": "change:capital_gains_tax:2027:7",
+      "case_id": "mid_year_blended_rate",
+      "check": "change:capital_gains_tax:2027",
+      "decision_id": null,
+      "expected": 60.81967213114759,
+      "holds": null,
+      "oracle": "(\u00a310,000 of gains - the annual exempt amount) x (20% - current rate) x the share of the tax year at 20%, with realisations held fixed; the gains sit wholly in the basic rate band, and the exempt amount and current rate are read from current law; 159 of 2027-28's 366 days",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 140.0,
+      "assertion_id": "change:capital_gains_tax:2028:8",
+      "case_id": "mid_year_blended_rate",
+      "check": "change:capital_gains_tax:2028",
+      "decision_id": null,
+      "expected": 139.99999999999994,
+      "holds": null,
+      "oracle": "(\u00a310,000 of gains - the annual exempt amount) x (20% - current rate) x the share of the tax year at 20%, with realisations held fixed; the gains sit wholly in the basic rate band, and the exempt amount and current rate are read from current law; all of 2028-29",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "baseline_isolation:capital_gains_tax:2026",
+      "expected": 1260.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "baseline_isolation:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "baseline_isolation:capital_gains_tax:2028",
+      "expected": 1260.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.18,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:gov.hmrc.cgt.basic_rate:2027",
+      "expected": 0.18,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.18000000000000002,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:gov.hmrc.cgt.basic_rate:2028",
+      "expected": 0.18000000000000002,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.18000000000000002,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:gov.hmrc.cgt.basic_rate:2026",
+      "expected": 0.18000000000000002,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:capital_gains_tax:2026",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:capital_gains_tax:2028",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "0059b42d41bee7074a7f0a19cf2e9590ce1562cb6874e147557f6aaba3bc1a0b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "5d2bd0b5a49e1642b8c3eb392de195c894cd9b44946abda008c23fc1a022fa90",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.18868852459016394,
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2027:0",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2027",
+      "decision_id": null,
+      "expected": 0.18868852459016502,
+      "holds": null,
+      "oracle": "day-weighted: 207 days at 18% and 159 at 20% of 366",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2028:1",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2028",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "in force all year",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.18000000000000002,
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2026:2",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2026",
+      "decision_id": null,
+      "expected": 0.18,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.elasticity:2027:3",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.simulation.capital_gains_responses.elasticity:2027",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "set by the case",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027:4",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "set by the case",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027:5",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "set by the case",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:capital_gains_tax:2026:6",
+      "case_id": "mid_year_blended_rate",
+      "check": "change:capital_gains_tax:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 60.8197021484375,
+      "assertion_id": "change:capital_gains_tax:2027:7",
+      "case_id": "mid_year_blended_rate",
+      "check": "change:capital_gains_tax:2027",
+      "decision_id": null,
+      "expected": 60.81967213114759,
+      "holds": null,
+      "oracle": "(\u00a310,000 of gains - the annual exempt amount) x (20% - current rate) x the share of the tax year at 20%, with realisations held fixed; the gains sit wholly in the basic rate band, and the exempt amount and current rate are read from current law; 159 of 2027-28's 366 days",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 140.0,
+      "assertion_id": "change:capital_gains_tax:2028:8",
+      "case_id": "mid_year_blended_rate",
+      "check": "change:capital_gains_tax:2028",
+      "decision_id": null,
+      "expected": 139.99999999999994,
+      "holds": null,
+      "oracle": "(\u00a310,000 of gains - the annual exempt amount) x (20% - current rate) x the share of the tax year at 20%, with realisations held fixed; the gains sit wholly in the basic rate band, and the exempt amount and current rate are read from current law; all of 2028-29",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "baseline_isolation:capital_gains_tax:2026",
+      "expected": 1260.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "baseline_isolation:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "baseline_isolation:capital_gains_tax:2028",
+      "expected": 1260.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.18,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:gov.hmrc.cgt.basic_rate:2027",
+      "expected": 0.18,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.18000000000000002,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:gov.hmrc.cgt.basic_rate:2028",
+      "expected": 0.18000000000000002,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.18000000000000002,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:gov.hmrc.cgt.basic_rate:2026",
+      "expected": 0.18000000000000002,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:capital_gains_tax:2026",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:capital_gains_tax:2028",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e648a4ec5d8f272d78a1128c21ed611c08a573f6c4a332d46f068bf72d164296",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2027:0",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "3b5170cc3cd245b56b3545d2b8c83bce7846ea472317ca194dff12ced488a05b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2028:1",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2028",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "3b5170cc3cd245b56b3545d2b8c83bce7846ea472317ca194dff12ced488a05b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2026:2",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2026",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "3b5170cc3cd245b56b3545d2b8c83bce7846ea472317ca194dff12ced488a05b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.elasticity:2027:3",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.simulation.capital_gains_responses.elasticity:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "3b5170cc3cd245b56b3545d2b8c83bce7846ea472317ca194dff12ced488a05b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027:4",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "3b5170cc3cd245b56b3545d2b8c83bce7846ea472317ca194dff12ced488a05b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027:5",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "3b5170cc3cd245b56b3545d2b8c83bce7846ea472317ca194dff12ced488a05b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:capital_gains_tax:2026:6",
+      "case_id": "mid_year_blended_rate",
+      "check": "change:capital_gains_tax:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "3b5170cc3cd245b56b3545d2b8c83bce7846ea472317ca194dff12ced488a05b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 60.8197021484375,
+      "assertion_id": "change:capital_gains_tax:2027:7",
+      "case_id": "mid_year_blended_rate",
+      "check": "change:capital_gains_tax:2027",
+      "decision_id": null,
+      "expected": 60.81967213114759,
+      "holds": null,
+      "oracle": "(\u00a310,000 of gains - the annual exempt amount) x (20% - current rate) x the share of the tax year at 20%, with realisations held fixed; the gains sit wholly in the basic rate band, and the exempt amount and current rate are read from current law; 159 of 2027-28's 366 days",
+      "reform_digest": "3b5170cc3cd245b56b3545d2b8c83bce7846ea472317ca194dff12ced488a05b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 140.0,
+      "assertion_id": "change:capital_gains_tax:2028:8",
+      "case_id": "mid_year_blended_rate",
+      "check": "change:capital_gains_tax:2028",
+      "decision_id": null,
+      "expected": 139.99999999999994,
+      "holds": null,
+      "oracle": "(\u00a310,000 of gains - the annual exempt amount) x (20% - current rate) x the share of the tax year at 20%, with realisations held fixed; the gains sit wholly in the basic rate band, and the exempt amount and current rate are read from current law; all of 2028-29",
+      "reform_digest": "3b5170cc3cd245b56b3545d2b8c83bce7846ea472317ca194dff12ced488a05b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:capital_gains_tax:2026",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "3b5170cc3cd245b56b3545d2b8c83bce7846ea472317ca194dff12ced488a05b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "3b5170cc3cd245b56b3545d2b8c83bce7846ea472317ca194dff12ced488a05b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:capital_gains_tax:2028",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "3b5170cc3cd245b56b3545d2b8c83bce7846ea472317ca194dff12ced488a05b",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2027:0",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "10b81ccc8d17c82f2ba48d5b76f3a79a839d78d4fb357baeffe41c3074ca7475",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2028:1",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2028",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "10b81ccc8d17c82f2ba48d5b76f3a79a839d78d4fb357baeffe41c3074ca7475",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2026:2",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2026",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "10b81ccc8d17c82f2ba48d5b76f3a79a839d78d4fb357baeffe41c3074ca7475",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.elasticity:2027:3",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.simulation.capital_gains_responses.elasticity:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "10b81ccc8d17c82f2ba48d5b76f3a79a839d78d4fb357baeffe41c3074ca7475",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027:4",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "10b81ccc8d17c82f2ba48d5b76f3a79a839d78d4fb357baeffe41c3074ca7475",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027:5",
+      "case_id": "mid_year_blended_rate",
+      "check": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "10b81ccc8d17c82f2ba48d5b76f3a79a839d78d4fb357baeffe41c3074ca7475",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:capital_gains_tax:2026:6",
+      "case_id": "mid_year_blended_rate",
+      "check": "change:capital_gains_tax:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "10b81ccc8d17c82f2ba48d5b76f3a79a839d78d4fb357baeffe41c3074ca7475",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 60.8197021484375,
+      "assertion_id": "change:capital_gains_tax:2027:7",
+      "case_id": "mid_year_blended_rate",
+      "check": "change:capital_gains_tax:2027",
+      "decision_id": null,
+      "expected": 60.81967213114759,
+      "holds": null,
+      "oracle": "(\u00a310,000 of gains - the annual exempt amount) x (20% - current rate) x the share of the tax year at 20%, with realisations held fixed; the gains sit wholly in the basic rate band, and the exempt amount and current rate are read from current law; 159 of 2027-28's 366 days",
+      "reform_digest": "10b81ccc8d17c82f2ba48d5b76f3a79a839d78d4fb357baeffe41c3074ca7475",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 140.0,
+      "assertion_id": "change:capital_gains_tax:2028:8",
+      "case_id": "mid_year_blended_rate",
+      "check": "change:capital_gains_tax:2028",
+      "decision_id": null,
+      "expected": 139.99999999999994,
+      "holds": null,
+      "oracle": "(\u00a310,000 of gains - the annual exempt amount) x (20% - current rate) x the share of the tax year at 20%, with realisations held fixed; the gains sit wholly in the basic rate band, and the exempt amount and current rate are read from current law; all of 2028-29",
+      "reform_digest": "10b81ccc8d17c82f2ba48d5b76f3a79a839d78d4fb357baeffe41c3074ca7475",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:capital_gains_tax:2026",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "10b81ccc8d17c82f2ba48d5b76f3a79a839d78d4fb357baeffe41c3074ca7475",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "10b81ccc8d17c82f2ba48d5b76f3a79a839d78d4fb357baeffe41c3074ca7475",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "mid_year_blended_rate",
+      "check": "unreformed_after_reform:capital_gains_tax:2028",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "10b81ccc8d17c82f2ba48d5b76f3a79a839d78d4fb357baeffe41c3074ca7475",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "mid_year_blended_rate",
+        "reform_sha256": "a7a024bbd0510652b8da6f8fbf64855b1f11f3debeb8cfbb0af0c3e5510d8db5",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "case_id": "mid_year_blended_rate",
+      "check": null,
+      "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+      "reform_digest": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "the route does not run in-process",
+      "route": "api.sim_api",
+      "status": "unsupported"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2027:0",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2027",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "in force all year",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.elasticity:2027:1",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.elasticity:2027",
+      "decision_id": null,
+      "expected": 1.0,
+      "holds": null,
+      "oracle": "set by the case",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027:2",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "set by the case",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027:3",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "set by the case",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:capital_gains_tax:2026:4",
+      "case_id": "cgt_retention_response",
+      "check": "change:capital_gains_tax:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "assertion_id": "variable:capital_gains:2026:5",
+      "case_id": "cgt_retention_response",
+      "check": "variable:capital_gains:2026",
+      "decision_id": null,
+      "expected": 10000.0,
+      "holds": null,
+      "oracle": "no rate change before 2027-28, so no response",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 9756.09765625,
+      "assertion_id": "variable:capital_gains:2027:6",
+      "case_id": "cgt_retention_response",
+      "check": "variable:capital_gains:2027",
+      "decision_id": null,
+      "expected": 9756.09756097561,
+      "holds": null,
+      "oracle": "\u00a310,000 of gains x ((1 - 20%) / (1 - current rate)) to the power of the elasticity: the gains sit wholly in the basic rate band, so the marginal rates on them are the basic rates; the current rate is read from current law",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 91.2196044921875,
+      "assertion_id": "change:capital_gains_tax:2027:7",
+      "case_id": "cgt_retention_response",
+      "check": "change:capital_gains_tax:2027",
+      "decision_id": null,
+      "expected": 91.21951219512198,
+      "holds": null,
+      "oracle": "(realised gains - the annual exempt amount) x 20% - (\u00a310,000 of gains - the annual exempt amount) x current rate, with the realised gains above; the exempt amount and current rate are read from current law",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_retention_response",
+      "check": "baseline_isolation:capital_gains_tax:2026",
+      "expected": 1260.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_retention_response",
+      "check": "baseline_isolation:capital_gains:2026",
+      "expected": 10000.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_retention_response",
+      "check": "baseline_isolation:capital_gains:2027",
+      "expected": 10000.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_retention_response",
+      "check": "baseline_isolation:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.18,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:gov.hmrc.cgt.basic_rate:2027",
+      "expected": 0.18,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains_tax:2026",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains:2026",
+      "expected": 10000.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains:2027",
+      "expected": 10000.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "9b906fb9819dd70ed26b3007c7219bd4de7fe8a13d34f1aab4c9bb8520e2af12",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "e8ad1a80f3c772e8125843e9caafda1a29bb70b536c992ff6c1cfc64ab411636",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2027:0",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2027",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "in force all year",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 1.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.elasticity:2027:1",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.elasticity:2027",
+      "decision_id": null,
+      "expected": 1.0,
+      "holds": null,
+      "oracle": "set by the case",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027:2",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "set by the case",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027:3",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "set by the case",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:capital_gains_tax:2026:4",
+      "case_id": "cgt_retention_response",
+      "check": "change:capital_gains_tax:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "assertion_id": "variable:capital_gains:2026:5",
+      "case_id": "cgt_retention_response",
+      "check": "variable:capital_gains:2026",
+      "decision_id": null,
+      "expected": 10000.0,
+      "holds": null,
+      "oracle": "no rate change before 2027-28, so no response",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 9756.09765625,
+      "assertion_id": "variable:capital_gains:2027:6",
+      "case_id": "cgt_retention_response",
+      "check": "variable:capital_gains:2027",
+      "decision_id": null,
+      "expected": 9756.09756097561,
+      "holds": null,
+      "oracle": "\u00a310,000 of gains x ((1 - 20%) / (1 - current rate)) to the power of the elasticity: the gains sit wholly in the basic rate band, so the marginal rates on them are the basic rates; the current rate is read from current law",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 91.2196044921875,
+      "assertion_id": "change:capital_gains_tax:2027:7",
+      "case_id": "cgt_retention_response",
+      "check": "change:capital_gains_tax:2027",
+      "decision_id": null,
+      "expected": 91.21951219512198,
+      "holds": null,
+      "oracle": "(realised gains - the annual exempt amount) x 20% - (\u00a310,000 of gains - the annual exempt amount) x current rate, with the realised gains above; the exempt amount and current rate are read from current law",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_retention_response",
+      "check": "baseline_isolation:capital_gains_tax:2026",
+      "expected": 1260.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_retention_response",
+      "check": "baseline_isolation:capital_gains:2026",
+      "expected": 10000.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_retention_response",
+      "check": "baseline_isolation:capital_gains:2027",
+      "expected": 10000.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_retention_response",
+      "check": "baseline_isolation:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.18,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:gov.hmrc.cgt.basic_rate:2027",
+      "expected": 0.18,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains_tax:2026",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains:2026",
+      "expected": 10000.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains:2027",
+      "expected": 10000.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "c5a197e63745011018a30ab33fb6818e06ca5f0a910c0738dd17e6ddc3007fcf",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "0362c64a95b43a701e46cb6dfe0ee72dcc1d9bc6bb5dd5357e6a4be7782c52e0",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2027:0",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2027",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "in force all year",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.elasticity:2027:1",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.elasticity:2027",
+      "decision_id": null,
+      "expected": 1.0,
+      "holds": null,
+      "oracle": "set by the case",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027:2",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "set by the case",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027:3",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "set by the case",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:capital_gains_tax:2026:4",
+      "case_id": "cgt_retention_response",
+      "check": "change:capital_gains_tax:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "assertion_id": "variable:capital_gains:2026:5",
+      "case_id": "cgt_retention_response",
+      "check": "variable:capital_gains:2026",
+      "decision_id": null,
+      "expected": 10000.0,
+      "holds": null,
+      "oracle": "no rate change before 2027-28, so no response",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 9756.09765625,
+      "assertion_id": "variable:capital_gains:2027:6",
+      "case_id": "cgt_retention_response",
+      "check": "variable:capital_gains:2027",
+      "decision_id": null,
+      "expected": 9756.09756097561,
+      "holds": null,
+      "oracle": "\u00a310,000 of gains x ((1 - 20%) / (1 - current rate)) to the power of the elasticity: the gains sit wholly in the basic rate band, so the marginal rates on them are the basic rates; the current rate is read from current law",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 91.2196044921875,
+      "assertion_id": "change:capital_gains_tax:2027:7",
+      "case_id": "cgt_retention_response",
+      "check": "change:capital_gains_tax:2027",
+      "decision_id": null,
+      "expected": 91.21951219512198,
+      "holds": null,
+      "oracle": "(realised gains - the annual exempt amount) x 20% - (\u00a310,000 of gains - the annual exempt amount) x current rate, with the realised gains above; the exempt amount and current rate are read from current law",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_retention_response",
+      "check": "baseline_isolation:capital_gains_tax:2026",
+      "expected": 1260.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_retention_response",
+      "check": "baseline_isolation:capital_gains:2026",
+      "expected": 10000.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_retention_response",
+      "check": "baseline_isolation:capital_gains:2027",
+      "expected": 10000.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_retention_response",
+      "check": "baseline_isolation:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.18,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:gov.hmrc.cgt.basic_rate:2027",
+      "expected": 0.18,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains_tax:2026",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains:2026",
+      "expected": 10000.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains:2027",
+      "expected": 10000.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "f1418e77536fc627923bd4334ebc6c54d3325ce309ad10c411236338881b03dc",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2027:0",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "a98f7df8e58f2b3387b99f3fb49d4e89b89d80fe96c5fe8c947346818a4dd929",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.elasticity:2027:1",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.elasticity:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "a98f7df8e58f2b3387b99f3fb49d4e89b89d80fe96c5fe8c947346818a4dd929",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027:2",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "a98f7df8e58f2b3387b99f3fb49d4e89b89d80fe96c5fe8c947346818a4dd929",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027:3",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "a98f7df8e58f2b3387b99f3fb49d4e89b89d80fe96c5fe8c947346818a4dd929",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:capital_gains_tax:2026:4",
+      "case_id": "cgt_retention_response",
+      "check": "change:capital_gains_tax:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "a98f7df8e58f2b3387b99f3fb49d4e89b89d80fe96c5fe8c947346818a4dd929",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "assertion_id": "variable:capital_gains:2026:5",
+      "case_id": "cgt_retention_response",
+      "check": "variable:capital_gains:2026",
+      "decision_id": null,
+      "expected": 10000.0,
+      "holds": null,
+      "oracle": "no rate change before 2027-28, so no response",
+      "reform_digest": "a98f7df8e58f2b3387b99f3fb49d4e89b89d80fe96c5fe8c947346818a4dd929",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 9756.09765625,
+      "assertion_id": "variable:capital_gains:2027:6",
+      "case_id": "cgt_retention_response",
+      "check": "variable:capital_gains:2027",
+      "decision_id": null,
+      "expected": 9756.09756097561,
+      "holds": null,
+      "oracle": "\u00a310,000 of gains x ((1 - 20%) / (1 - current rate)) to the power of the elasticity: the gains sit wholly in the basic rate band, so the marginal rates on them are the basic rates; the current rate is read from current law",
+      "reform_digest": "a98f7df8e58f2b3387b99f3fb49d4e89b89d80fe96c5fe8c947346818a4dd929",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 91.2196044921875,
+      "assertion_id": "change:capital_gains_tax:2027:7",
+      "case_id": "cgt_retention_response",
+      "check": "change:capital_gains_tax:2027",
+      "decision_id": null,
+      "expected": 91.21951219512198,
+      "holds": null,
+      "oracle": "(realised gains - the annual exempt amount) x 20% - (\u00a310,000 of gains - the annual exempt amount) x current rate, with the realised gains above; the exempt amount and current rate are read from current law",
+      "reform_digest": "a98f7df8e58f2b3387b99f3fb49d4e89b89d80fe96c5fe8c947346818a4dd929",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains:2026",
+      "expected": 10000.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "a98f7df8e58f2b3387b99f3fb49d4e89b89d80fe96c5fe8c947346818a4dd929",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains_tax:2026",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "a98f7df8e58f2b3387b99f3fb49d4e89b89d80fe96c5fe8c947346818a4dd929",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains:2027",
+      "expected": 10000.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "a98f7df8e58f2b3387b99f3fb49d4e89b89d80fe96c5fe8c947346818a4dd929",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "a98f7df8e58f2b3387b99f3fb49d4e89b89d80fe96c5fe8c947346818a4dd929",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.cgt.basic_rate:2027:0",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.hmrc.cgt.basic_rate:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "fd7e26a199e3488efbfdb64444be1cb27181109336dcd506dd61c7d960bba1de",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.elasticity:2027:1",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.elasticity:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "fd7e26a199e3488efbfdb64444be1cb27181109336dcd506dd61c7d960bba1de",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027:2",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "fd7e26a199e3488efbfdb64444be1cb27181109336dcd506dd61c7d960bba1de",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027:3",
+      "case_id": "cgt_retention_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "fd7e26a199e3488efbfdb64444be1cb27181109336dcd506dd61c7d960bba1de",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "change:capital_gains_tax:2026:4",
+      "case_id": "cgt_retention_response",
+      "check": "change:capital_gains_tax:2026",
+      "decision_id": null,
+      "expected": 0.0,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "fd7e26a199e3488efbfdb64444be1cb27181109336dcd506dd61c7d960bba1de",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "assertion_id": "variable:capital_gains:2026:5",
+      "case_id": "cgt_retention_response",
+      "check": "variable:capital_gains:2026",
+      "decision_id": null,
+      "expected": 10000.0,
+      "holds": null,
+      "oracle": "no rate change before 2027-28, so no response",
+      "reform_digest": "fd7e26a199e3488efbfdb64444be1cb27181109336dcd506dd61c7d960bba1de",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 9756.09765625,
+      "assertion_id": "variable:capital_gains:2027:6",
+      "case_id": "cgt_retention_response",
+      "check": "variable:capital_gains:2027",
+      "decision_id": null,
+      "expected": 9756.09756097561,
+      "holds": null,
+      "oracle": "\u00a310,000 of gains x ((1 - 20%) / (1 - current rate)) to the power of the elasticity: the gains sit wholly in the basic rate band, so the marginal rates on them are the basic rates; the current rate is read from current law",
+      "reform_digest": "fd7e26a199e3488efbfdb64444be1cb27181109336dcd506dd61c7d960bba1de",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 91.2196044921875,
+      "assertion_id": "change:capital_gains_tax:2027:7",
+      "case_id": "cgt_retention_response",
+      "check": "change:capital_gains_tax:2027",
+      "decision_id": null,
+      "expected": 91.21951219512198,
+      "holds": null,
+      "oracle": "(realised gains - the annual exempt amount) x 20% - (\u00a310,000 of gains - the annual exempt amount) x current rate, with the realised gains above; the exempt amount and current rate are read from current law",
+      "reform_digest": "fd7e26a199e3488efbfdb64444be1cb27181109336dcd506dd61c7d960bba1de",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains:2026",
+      "expected": 10000.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fd7e26a199e3488efbfdb64444be1cb27181109336dcd506dd61c7d960bba1de",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains_tax:2026",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fd7e26a199e3488efbfdb64444be1cb27181109336dcd506dd61c7d960bba1de",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains:2027",
+      "expected": 10000.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fd7e26a199e3488efbfdb64444be1cb27181109336dcd506dd61c7d960bba1de",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_retention_response",
+      "check": "unreformed_after_reform:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fd7e26a199e3488efbfdb64444be1cb27181109336dcd506dd61c7d960bba1de",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_retention_response",
+        "reform_sha256": "49e775a750c127c8dda5bed6e5651145d63d3330d50a836a460e64e2e81bb9d3",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "case_id": "cgt_retention_response",
+      "check": null,
+      "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+      "reform_digest": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "the route does not run in-process",
+      "route": "api.sim_api",
+      "status": "unsupported"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.elasticity:2027:0",
+      "case_id": "cgt_default_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.elasticity:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 1.0,
+      "holds": false,
+      "oracle": "policyengine-uk's default",
+      "reform_digest": "b979620ae16eb9d24e950ef679de4d8a60c71049f84205fabdfbaad610e64c18",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "18e7b253186aea4b35258a26537cf8ee7fa6f8754a8eb762dd217b59b7b372b7",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pending"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027:1",
+      "case_id": "cgt_default_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 0.0,
+      "holds": true,
+      "oracle": "policyengine-uk's default",
+      "reform_digest": "b979620ae16eb9d24e950ef679de4d8a60c71049f84205fabdfbaad610e64c18",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "18e7b253186aea4b35258a26537cf8ee7fa6f8754a8eb762dd217b59b7b372b7",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pending"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027:2",
+      "case_id": "cgt_default_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 0.0,
+      "holds": true,
+      "oracle": "policyengine-uk's default",
+      "reform_digest": "b979620ae16eb9d24e950ef679de4d8a60c71049f84205fabdfbaad610e64c18",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "18e7b253186aea4b35258a26537cf8ee7fa6f8754a8eb762dd217b59b7b372b7",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pending"
+    },
+    {
+      "actual": 10000.0,
+      "assertion_id": "variable:capital_gains:2027:3",
+      "case_id": "cgt_default_response",
+      "check": "variable:capital_gains:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 9756.09756097561,
+      "holds": false,
+      "oracle": "\u00a310,000 of gains x ((1 - 20%) / (1 - current rate)) to the power of the elasticity: the gains sit wholly in the basic rate band, so the marginal rates on them are the basic rates; the current rate is read from current law, at the default elasticity of 1",
+      "reform_digest": "b979620ae16eb9d24e950ef679de4d8a60c71049f84205fabdfbaad610e64c18",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "18e7b253186aea4b35258a26537cf8ee7fa6f8754a8eb762dd217b59b7b372b7",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pending"
+    },
+    {
+      "actual": 140.0,
+      "assertion_id": "change:capital_gains_tax:2027:4",
+      "case_id": "cgt_default_response",
+      "check": "change:capital_gains_tax:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 91.21951219512198,
+      "holds": false,
+      "oracle": "(realised gains - the annual exempt amount) x 20% - (\u00a310,000 of gains - the annual exempt amount) x current rate, with the realised gains above; the exempt amount and current rate are read from current law, at the default elasticity of 1",
+      "reform_digest": "b979620ae16eb9d24e950ef679de4d8a60c71049f84205fabdfbaad610e64c18",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "18e7b253186aea4b35258a26537cf8ee7fa6f8754a8eb762dd217b59b7b372b7",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pending"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_default_response",
+      "check": "baseline_isolation:capital_gains:2027",
+      "expected": 10000.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "b979620ae16eb9d24e950ef679de4d8a60c71049f84205fabdfbaad610e64c18",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "18e7b253186aea4b35258a26537cf8ee7fa6f8754a8eb762dd217b59b7b372b7",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_default_response",
+      "check": "baseline_isolation:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "b979620ae16eb9d24e950ef679de4d8a60c71049f84205fabdfbaad610e64c18",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "18e7b253186aea4b35258a26537cf8ee7fa6f8754a8eb762dd217b59b7b372b7",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "b979620ae16eb9d24e950ef679de4d8a60c71049f84205fabdfbaad610e64c18",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "18e7b253186aea4b35258a26537cf8ee7fa6f8754a8eb762dd217b59b7b372b7",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "b979620ae16eb9d24e950ef679de4d8a60c71049f84205fabdfbaad610e64c18",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "18e7b253186aea4b35258a26537cf8ee7fa6f8754a8eb762dd217b59b7b372b7",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "b979620ae16eb9d24e950ef679de4d8a60c71049f84205fabdfbaad610e64c18",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "18e7b253186aea4b35258a26537cf8ee7fa6f8754a8eb762dd217b59b7b372b7",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:capital_gains:2027",
+      "expected": 10000.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "b979620ae16eb9d24e950ef679de4d8a60c71049f84205fabdfbaad610e64c18",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "18e7b253186aea4b35258a26537cf8ee7fa6f8754a8eb762dd217b59b7b372b7",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "b979620ae16eb9d24e950ef679de4d8a60c71049f84205fabdfbaad610e64c18",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "18e7b253186aea4b35258a26537cf8ee7fa6f8754a8eb762dd217b59b7b372b7",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.elasticity:2027:0",
+      "case_id": "cgt_default_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.elasticity:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 1.0,
+      "holds": false,
+      "oracle": "policyengine-uk's default",
+      "reform_digest": "fd0f0568eb35b8455afcad278ed5e34a65d9520e6e79e16e9e5368ab535a0039",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "d9d5ec83a4ed5dd9a7efa2ead3ebc680e98472a1f5996b7845cbe5d9a586254b",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pending"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027:1",
+      "case_id": "cgt_default_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 0.0,
+      "holds": true,
+      "oracle": "policyengine-uk's default",
+      "reform_digest": "fd0f0568eb35b8455afcad278ed5e34a65d9520e6e79e16e9e5368ab535a0039",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "d9d5ec83a4ed5dd9a7efa2ead3ebc680e98472a1f5996b7845cbe5d9a586254b",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pending"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027:2",
+      "case_id": "cgt_default_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 0.0,
+      "holds": true,
+      "oracle": "policyengine-uk's default",
+      "reform_digest": "fd0f0568eb35b8455afcad278ed5e34a65d9520e6e79e16e9e5368ab535a0039",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "d9d5ec83a4ed5dd9a7efa2ead3ebc680e98472a1f5996b7845cbe5d9a586254b",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pending"
+    },
+    {
+      "actual": 10000.0,
+      "assertion_id": "variable:capital_gains:2027:3",
+      "case_id": "cgt_default_response",
+      "check": "variable:capital_gains:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 9756.09756097561,
+      "holds": false,
+      "oracle": "\u00a310,000 of gains x ((1 - 20%) / (1 - current rate)) to the power of the elasticity: the gains sit wholly in the basic rate band, so the marginal rates on them are the basic rates; the current rate is read from current law, at the default elasticity of 1",
+      "reform_digest": "fd0f0568eb35b8455afcad278ed5e34a65d9520e6e79e16e9e5368ab535a0039",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "d9d5ec83a4ed5dd9a7efa2ead3ebc680e98472a1f5996b7845cbe5d9a586254b",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pending"
+    },
+    {
+      "actual": 140.0,
+      "assertion_id": "change:capital_gains_tax:2027:4",
+      "case_id": "cgt_default_response",
+      "check": "change:capital_gains_tax:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 91.21951219512198,
+      "holds": false,
+      "oracle": "(realised gains - the annual exempt amount) x 20% - (\u00a310,000 of gains - the annual exempt amount) x current rate, with the realised gains above; the exempt amount and current rate are read from current law, at the default elasticity of 1",
+      "reform_digest": "fd0f0568eb35b8455afcad278ed5e34a65d9520e6e79e16e9e5368ab535a0039",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "d9d5ec83a4ed5dd9a7efa2ead3ebc680e98472a1f5996b7845cbe5d9a586254b",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pending"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_default_response",
+      "check": "baseline_isolation:capital_gains:2027",
+      "expected": 10000.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "fd0f0568eb35b8455afcad278ed5e34a65d9520e6e79e16e9e5368ab535a0039",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "d9d5ec83a4ed5dd9a7efa2ead3ebc680e98472a1f5996b7845cbe5d9a586254b",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_default_response",
+      "check": "baseline_isolation:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "fd0f0568eb35b8455afcad278ed5e34a65d9520e6e79e16e9e5368ab535a0039",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "d9d5ec83a4ed5dd9a7efa2ead3ebc680e98472a1f5996b7845cbe5d9a586254b",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fd0f0568eb35b8455afcad278ed5e34a65d9520e6e79e16e9e5368ab535a0039",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "d9d5ec83a4ed5dd9a7efa2ead3ebc680e98472a1f5996b7845cbe5d9a586254b",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fd0f0568eb35b8455afcad278ed5e34a65d9520e6e79e16e9e5368ab535a0039",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "d9d5ec83a4ed5dd9a7efa2ead3ebc680e98472a1f5996b7845cbe5d9a586254b",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fd0f0568eb35b8455afcad278ed5e34a65d9520e6e79e16e9e5368ab535a0039",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "d9d5ec83a4ed5dd9a7efa2ead3ebc680e98472a1f5996b7845cbe5d9a586254b",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:capital_gains:2027",
+      "expected": 10000.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fd0f0568eb35b8455afcad278ed5e34a65d9520e6e79e16e9e5368ab535a0039",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "d9d5ec83a4ed5dd9a7efa2ead3ebc680e98472a1f5996b7845cbe5d9a586254b",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fd0f0568eb35b8455afcad278ed5e34a65d9520e6e79e16e9e5368ab535a0039",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "d9d5ec83a4ed5dd9a7efa2ead3ebc680e98472a1f5996b7845cbe5d9a586254b",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.elasticity:2027:0",
+      "case_id": "cgt_default_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.elasticity:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 1.0,
+      "holds": false,
+      "oracle": "policyengine-uk's default",
+      "reform_digest": "52bd8c4df9f94da32c4f7f498f38c2aeef358a10348789e0fbe91a76f8d9834e",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pending"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027:1",
+      "case_id": "cgt_default_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 0.0,
+      "holds": true,
+      "oracle": "policyengine-uk's default",
+      "reform_digest": "52bd8c4df9f94da32c4f7f498f38c2aeef358a10348789e0fbe91a76f8d9834e",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pending"
+    },
+    {
+      "actual": 0.0,
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027:2",
+      "case_id": "cgt_default_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 0.0,
+      "holds": true,
+      "oracle": "policyengine-uk's default",
+      "reform_digest": "52bd8c4df9f94da32c4f7f498f38c2aeef358a10348789e0fbe91a76f8d9834e",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pending"
+    },
+    {
+      "actual": 10000.0,
+      "assertion_id": "variable:capital_gains:2027:3",
+      "case_id": "cgt_default_response",
+      "check": "variable:capital_gains:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 9756.09756097561,
+      "holds": false,
+      "oracle": "\u00a310,000 of gains x ((1 - 20%) / (1 - current rate)) to the power of the elasticity: the gains sit wholly in the basic rate band, so the marginal rates on them are the basic rates; the current rate is read from current law, at the default elasticity of 1",
+      "reform_digest": "52bd8c4df9f94da32c4f7f498f38c2aeef358a10348789e0fbe91a76f8d9834e",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pending"
+    },
+    {
+      "actual": 140.0,
+      "assertion_id": "change:capital_gains_tax:2027:4",
+      "case_id": "cgt_default_response",
+      "check": "change:capital_gains_tax:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 91.21951219512198,
+      "holds": false,
+      "oracle": "(realised gains - the annual exempt amount) x 20% - (\u00a310,000 of gains - the annual exempt amount) x current rate, with the realised gains above; the exempt amount and current rate are read from current law, at the default elasticity of 1",
+      "reform_digest": "52bd8c4df9f94da32c4f7f498f38c2aeef358a10348789e0fbe91a76f8d9834e",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pending"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_default_response",
+      "check": "baseline_isolation:capital_gains:2027",
+      "expected": 10000.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "52bd8c4df9f94da32c4f7f498f38c2aeef358a10348789e0fbe91a76f8d9834e",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_default_response",
+      "check": "baseline_isolation:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "52bd8c4df9f94da32c4f7f498f38c2aeef358a10348789e0fbe91a76f8d9834e",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "52bd8c4df9f94da32c4f7f498f38c2aeef358a10348789e0fbe91a76f8d9834e",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "52bd8c4df9f94da32c4f7f498f38c2aeef358a10348789e0fbe91a76f8d9834e",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "52bd8c4df9f94da32c4f7f498f38c2aeef358a10348789e0fbe91a76f8d9834e",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:capital_gains:2027",
+      "expected": 10000.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "52bd8c4df9f94da32c4f7f498f38c2aeef358a10348789e0fbe91a76f8d9834e",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "52bd8c4df9f94da32c4f7f498f38c2aeef358a10348789e0fbe91a76f8d9834e",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.elasticity:2027:0",
+      "case_id": "cgt_default_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.elasticity:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "3ebd86525cf3d9fa26d8b326b94c85e358440ba7def9f8062ca337aef583ca68",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "b993d90de94b43bcbb2748db127c6e4f71e190bd8ff72e816eafb937eec9a9c8",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027:1",
+      "case_id": "cgt_default_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "3ebd86525cf3d9fa26d8b326b94c85e358440ba7def9f8062ca337aef583ca68",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "b993d90de94b43bcbb2748db127c6e4f71e190bd8ff72e816eafb937eec9a9c8",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027:2",
+      "case_id": "cgt_default_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "3ebd86525cf3d9fa26d8b326b94c85e358440ba7def9f8062ca337aef583ca68",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "b993d90de94b43bcbb2748db127c6e4f71e190bd8ff72e816eafb937eec9a9c8",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "unsupported"
+    },
+    {
+      "actual": 10000.0,
+      "assertion_id": "variable:capital_gains:2027:3",
+      "case_id": "cgt_default_response",
+      "check": "variable:capital_gains:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 9756.09756097561,
+      "holds": false,
+      "oracle": "\u00a310,000 of gains x ((1 - 20%) / (1 - current rate)) to the power of the elasticity: the gains sit wholly in the basic rate band, so the marginal rates on them are the basic rates; the current rate is read from current law, at the default elasticity of 1",
+      "reform_digest": "3ebd86525cf3d9fa26d8b326b94c85e358440ba7def9f8062ca337aef583ca68",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "b993d90de94b43bcbb2748db127c6e4f71e190bd8ff72e816eafb937eec9a9c8",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pending"
+    },
+    {
+      "actual": 140.0,
+      "assertion_id": "change:capital_gains_tax:2027:4",
+      "case_id": "cgt_default_response",
+      "check": "change:capital_gains_tax:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 91.21951219512198,
+      "holds": false,
+      "oracle": "(realised gains - the annual exempt amount) x 20% - (\u00a310,000 of gains - the annual exempt amount) x current rate, with the realised gains above; the exempt amount and current rate are read from current law, at the default elasticity of 1",
+      "reform_digest": "3ebd86525cf3d9fa26d8b326b94c85e358440ba7def9f8062ca337aef583ca68",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "b993d90de94b43bcbb2748db127c6e4f71e190bd8ff72e816eafb937eec9a9c8",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pending"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:capital_gains:2027",
+      "expected": 10000.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "3ebd86525cf3d9fa26d8b326b94c85e358440ba7def9f8062ca337aef583ca68",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "b993d90de94b43bcbb2748db127c6e4f71e190bd8ff72e816eafb937eec9a9c8",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "3ebd86525cf3d9fa26d8b326b94c85e358440ba7def9f8062ca337aef583ca68",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "b993d90de94b43bcbb2748db127c6e4f71e190bd8ff72e816eafb937eec9a9c8",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.elasticity:2027:0",
+      "case_id": "cgt_default_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.elasticity:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "74ad41e5df137075fc41759a886fd4f66e0ea945e1d0c207429a90ff704413cd",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "b993d90de94b43bcbb2748db127c6e4f71e190bd8ff72e816eafb937eec9a9c8",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027:1",
+      "case_id": "cgt_default_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.mtr_elasticity:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "74ad41e5df137075fc41759a886fd4f66e0ea945e1d0c207429a90ff704413cd",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "b993d90de94b43bcbb2748db127c6e4f71e190bd8ff72e816eafb937eec9a9c8",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027:2",
+      "case_id": "cgt_default_response",
+      "check": "parameter:gov.simulation.capital_gains_responses.separate_badr_elasticity:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "74ad41e5df137075fc41759a886fd4f66e0ea945e1d0c207429a90ff704413cd",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "b993d90de94b43bcbb2748db127c6e4f71e190bd8ff72e816eafb937eec9a9c8",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "actual": 10000.0,
+      "assertion_id": "variable:capital_gains:2027:3",
+      "case_id": "cgt_default_response",
+      "check": "variable:capital_gains:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 9756.09756097561,
+      "holds": false,
+      "oracle": "\u00a310,000 of gains x ((1 - 20%) / (1 - current rate)) to the power of the elasticity: the gains sit wholly in the basic rate band, so the marginal rates on them are the basic rates; the current rate is read from current law, at the default elasticity of 1",
+      "reform_digest": "74ad41e5df137075fc41759a886fd4f66e0ea945e1d0c207429a90ff704413cd",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "b993d90de94b43bcbb2748db127c6e4f71e190bd8ff72e816eafb937eec9a9c8",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pending"
+    },
+    {
+      "actual": 140.0,
+      "assertion_id": "change:capital_gains_tax:2027:4",
+      "case_id": "cgt_default_response",
+      "check": "change:capital_gains_tax:2027",
+      "decision_id": "policyengine-uk#2396",
+      "expected": 91.21951219512198,
+      "holds": false,
+      "oracle": "(realised gains - the annual exempt amount) x 20% - (\u00a310,000 of gains - the annual exempt amount) x current rate, with the realised gains above; the exempt amount and current rate are read from current law, at the default elasticity of 1",
+      "reform_digest": "74ad41e5df137075fc41759a886fd4f66e0ea945e1d0c207429a90ff704413cd",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "b993d90de94b43bcbb2748db127c6e4f71e190bd8ff72e816eafb937eec9a9c8",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pending"
+    },
+    {
+      "actual": 10000.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:capital_gains:2027",
+      "expected": 10000.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "74ad41e5df137075fc41759a886fd4f66e0ea945e1d0c207429a90ff704413cd",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "b993d90de94b43bcbb2748db127c6e4f71e190bd8ff72e816eafb937eec9a9c8",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "case_id": "cgt_default_response",
+      "check": "unreformed_after_reform:capital_gains_tax:2027",
+      "expected": 1260.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "74ad41e5df137075fc41759a886fd4f66e0ea945e1d0c207429a90ff704413cd",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "cgt_default_response",
+        "reform_sha256": "b993d90de94b43bcbb2748db127c6e4f71e190bd8ff72e816eafb937eec9a9c8",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "case_id": "cgt_default_response",
+      "check": null,
+      "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+      "reform_digest": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "the route does not run in-process",
+      "route": "api.sim_api",
+      "status": "unsupported"
+    },
+    {
+      "actual": 13200.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031:0",
+      "case_id": "september_cpi_moves_indexed_thresholds",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031",
+      "decision_id": null,
+      "expected": 13200.0,
+      "holds": null,
+      "oracle": "ITA 2007 s57(3)",
+      "reform_digest": "8bd9476925c59e9d13946308e4604e33976dc8a1d68b63e276628c95135a82d0",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "september_cpi_moves_indexed_thresholds",
+        "reform_sha256": "9d7a12f52e9da74f616d30f23f8a8796ee7638e39ecebb63da492de0124aa0e9",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 39600.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031:1",
+      "case_id": "september_cpi_moves_indexed_thresholds",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031",
+      "decision_id": null,
+      "expected": 39600.0,
+      "holds": null,
+      "oracle": "ITA 2007 s21(3)",
+      "reform_digest": "8bd9476925c59e9d13946308e4604e33976dc8a1d68b63e276628c95135a82d0",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "september_cpi_moves_indexed_thresholds",
+        "reform_sha256": "9d7a12f52e9da74f616d30f23f8a8796ee7638e39ecebb63da492de0124aa0e9",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 253.85,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031:2",
+      "case_id": "september_cpi_moves_indexed_thresholds",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031",
+      "decision_id": null,
+      "expected": 253.85,
+      "holds": null,
+      "oracle": "aligned with the allowance, weekly",
+      "reform_digest": "8bd9476925c59e9d13946308e4604e33976dc8a1d68b63e276628c95135a82d0",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "september_cpi_moves_indexed_thresholds",
+        "reform_sha256": "9d7a12f52e9da74f616d30f23f8a8796ee7638e39ecebb63da492de0124aa0e9",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12830.0,
+      "case_id": "september_cpi_moves_indexed_thresholds",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031",
+      "expected": 12830.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "8bd9476925c59e9d13946308e4604e33976dc8a1d68b63e276628c95135a82d0",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "september_cpi_moves_indexed_thresholds",
+        "reform_sha256": "9d7a12f52e9da74f616d30f23f8a8796ee7638e39ecebb63da492de0124aa0e9",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 38500.0,
+      "case_id": "september_cpi_moves_indexed_thresholds",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[1].threshold:2031",
+      "expected": 38500.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "8bd9476925c59e9d13946308e4604e33976dc8a1d68b63e276628c95135a82d0",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "september_cpi_moves_indexed_thresholds",
+        "reform_sha256": "9d7a12f52e9da74f616d30f23f8a8796ee7638e39ecebb63da492de0124aa0e9",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 246.73,
+      "case_id": "september_cpi_moves_indexed_thresholds",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031",
+      "expected": 246.73,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "8bd9476925c59e9d13946308e4604e33976dc8a1d68b63e276628c95135a82d0",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "september_cpi_moves_indexed_thresholds",
+        "reform_sha256": "9d7a12f52e9da74f616d30f23f8a8796ee7638e39ecebb63da492de0124aa0e9",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 13200.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031:0",
+      "case_id": "september_cpi_moves_indexed_thresholds",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031",
+      "decision_id": null,
+      "expected": 13200.0,
+      "holds": null,
+      "oracle": "ITA 2007 s57(3)",
+      "reform_digest": "afe5d61179ae6fd02453405015018bcbdd8047fcc3e69e23c43e5f57dce17c67",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "september_cpi_moves_indexed_thresholds",
+        "reform_sha256": "3a203256153655059f114668934f03a8b0128524b1844c8bb72e50cb61f2f6e5",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 39600.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031:1",
+      "case_id": "september_cpi_moves_indexed_thresholds",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031",
+      "decision_id": null,
+      "expected": 39600.0,
+      "holds": null,
+      "oracle": "ITA 2007 s21(3)",
+      "reform_digest": "afe5d61179ae6fd02453405015018bcbdd8047fcc3e69e23c43e5f57dce17c67",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "september_cpi_moves_indexed_thresholds",
+        "reform_sha256": "3a203256153655059f114668934f03a8b0128524b1844c8bb72e50cb61f2f6e5",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 253.85,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031:2",
+      "case_id": "september_cpi_moves_indexed_thresholds",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031",
+      "decision_id": null,
+      "expected": 253.85,
+      "holds": null,
+      "oracle": "aligned with the allowance, weekly",
+      "reform_digest": "afe5d61179ae6fd02453405015018bcbdd8047fcc3e69e23c43e5f57dce17c67",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "september_cpi_moves_indexed_thresholds",
+        "reform_sha256": "3a203256153655059f114668934f03a8b0128524b1844c8bb72e50cb61f2f6e5",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 12830.0,
+      "case_id": "september_cpi_moves_indexed_thresholds",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031",
+      "expected": 12830.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "afe5d61179ae6fd02453405015018bcbdd8047fcc3e69e23c43e5f57dce17c67",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "september_cpi_moves_indexed_thresholds",
+        "reform_sha256": "3a203256153655059f114668934f03a8b0128524b1844c8bb72e50cb61f2f6e5",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 38500.0,
+      "case_id": "september_cpi_moves_indexed_thresholds",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[1].threshold:2031",
+      "expected": 38500.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "afe5d61179ae6fd02453405015018bcbdd8047fcc3e69e23c43e5f57dce17c67",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "september_cpi_moves_indexed_thresholds",
+        "reform_sha256": "3a203256153655059f114668934f03a8b0128524b1844c8bb72e50cb61f2f6e5",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 246.73,
+      "case_id": "september_cpi_moves_indexed_thresholds",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031",
+      "expected": 246.73,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "afe5d61179ae6fd02453405015018bcbdd8047fcc3e69e23c43e5f57dce17c67",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "september_cpi_moves_indexed_thresholds",
+        "reform_sha256": "3a203256153655059f114668934f03a8b0128524b1844c8bb72e50cb61f2f6e5",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 13200.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031:0",
+      "case_id": "september_cpi_moves_indexed_thresholds",
+      "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031",
+      "decision_id": null,
+      "expected": 13200.0,
+      "holds": null,
+      "oracle": "ITA 2007 s57(3)",
+      "reform_digest": "fda4228463d1ee4cd7263094d2b28ffb49c78d09b87468542dba7e6821ff3f9d",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 39600.0,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031:1",
+      "case_id": "september_cpi_moves_indexed_thresholds",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031",
+      "decision_id": null,
+      "expected": 39600.0,
+      "holds": null,
+      "oracle": "ITA 2007 s21(3)",
+      "reform_digest": "fda4228463d1ee4cd7263094d2b28ffb49c78d09b87468542dba7e6821ff3f9d",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 253.85,
+      "assertion_id": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031:2",
+      "case_id": "september_cpi_moves_indexed_thresholds",
+      "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031",
+      "decision_id": null,
+      "expected": 253.85,
+      "holds": null,
+      "oracle": "aligned with the allowance, weekly",
+      "reform_digest": "fda4228463d1ee4cd7263094d2b28ffb49c78d09b87468542dba7e6821ff3f9d",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 12830.0,
+      "case_id": "september_cpi_moves_indexed_thresholds",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031",
+      "expected": 12830.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fda4228463d1ee4cd7263094d2b28ffb49c78d09b87468542dba7e6821ff3f9d",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 38500.0,
+      "case_id": "september_cpi_moves_indexed_thresholds",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[1].threshold:2031",
+      "expected": 38500.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fda4228463d1ee4cd7263094d2b28ffb49c78d09b87468542dba7e6821ff3f9d",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 246.73,
+      "case_id": "september_cpi_moves_indexed_thresholds",
+      "check": "unreformed_after_reform:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031",
+      "expected": 246.73,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fda4228463d1ee4cd7263094d2b28ffb49c78d09b87468542dba7e6821ff3f9d",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:uc_LCWRA_element:2027:0",
+      "case_id": "uc_rebalancing_reform_after_data_load",
+      "check": "variable:uc_LCWRA_element:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "\u00a3500 x 12",
+      "reform_digest": "12bcb296368c2b63f30b2040bfa5c84678f0bd52aaf7f85d510a75cd70f995c3",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_rebalancing_reform_after_data_load",
+        "reform_sha256": "442bf59ad324711999e26e687b4acb9e421a35195346a4a2297354e9f255db7d",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5259.26806640625,
+      "case_id": "uc_rebalancing_reform_after_data_load",
+      "check": "baseline_isolation:uc_LCWRA_element:2027",
+      "expected": 5259.26806640625,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "12bcb296368c2b63f30b2040bfa5c84678f0bd52aaf7f85d510a75cd70f995c3",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_rebalancing_reform_after_data_load",
+        "reform_sha256": "442bf59ad324711999e26e687b4acb9e421a35195346a4a2297354e9f255db7d",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5259.26806640625,
+      "case_id": "uc_rebalancing_reform_after_data_load",
+      "check": "unreformed_after_reform:uc_LCWRA_element:2027",
+      "expected": 5259.26806640625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "12bcb296368c2b63f30b2040bfa5c84678f0bd52aaf7f85d510a75cd70f995c3",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_rebalancing_reform_after_data_load",
+        "reform_sha256": "442bf59ad324711999e26e687b4acb9e421a35195346a4a2297354e9f255db7d",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:uc_LCWRA_element:2027:0",
+      "case_id": "uc_rebalancing_reform_after_data_load",
+      "check": "variable:uc_LCWRA_element:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "\u00a3500 x 12",
+      "reform_digest": "909e6875b8d449e87b5d3eb0ad74e584e80241b9f40677343430b41232b20c5f",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_rebalancing_reform_after_data_load",
+        "reform_sha256": "9d0cecf62201d525f32c2b53d0cc5950127290a4063f4f709b09fff3ea3bd140",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5259.26806640625,
+      "case_id": "uc_rebalancing_reform_after_data_load",
+      "check": "baseline_isolation:uc_LCWRA_element:2027",
+      "expected": 5259.26806640625,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "909e6875b8d449e87b5d3eb0ad74e584e80241b9f40677343430b41232b20c5f",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_rebalancing_reform_after_data_load",
+        "reform_sha256": "9d0cecf62201d525f32c2b53d0cc5950127290a4063f4f709b09fff3ea3bd140",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5259.26806640625,
+      "case_id": "uc_rebalancing_reform_after_data_load",
+      "check": "unreformed_after_reform:uc_LCWRA_element:2027",
+      "expected": 5259.26806640625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "909e6875b8d449e87b5d3eb0ad74e584e80241b9f40677343430b41232b20c5f",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_rebalancing_reform_after_data_load",
+        "reform_sha256": "9d0cecf62201d525f32c2b53d0cc5950127290a4063f4f709b09fff3ea3bd140",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:uc_LCWRA_element:2027:0",
+      "case_id": "uc_rebalancing_reform_after_data_load",
+      "check": "variable:uc_LCWRA_element:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "\u00a3500 x 12",
+      "reform_digest": "9947c105e6b609c8a5f2c13ce4707daad40cfa8529bf2487734ef2a513169e8d",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5259.26806640625,
+      "case_id": "uc_rebalancing_reform_after_data_load",
+      "check": "baseline_isolation:uc_LCWRA_element:2027",
+      "expected": 5259.26806640625,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "9947c105e6b609c8a5f2c13ce4707daad40cfa8529bf2487734ef2a513169e8d",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5259.26806640625,
+      "case_id": "uc_rebalancing_reform_after_data_load",
+      "check": "unreformed_after_reform:uc_LCWRA_element:2027",
+      "expected": 5259.26806640625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "9947c105e6b609c8a5f2c13ce4707daad40cfa8529bf2487734ef2a513169e8d",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:uc_LCWRA_element:2027:0",
+      "case_id": "uc_rebalancing_reform_after_data_load",
+      "check": "variable:uc_LCWRA_element:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "\u00a3500 x 12",
+      "reform_digest": "a695bb7865a505dbbcb9c973b674a5666804e15838252c66bb7e84fa082388f0",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_rebalancing_reform_after_data_load",
+        "reform_sha256": "6f085d080cf6be93434e99ae984380f8ce87bdd6beab25c1abe4f1c9c8925a77",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 5259.26806640625,
+      "case_id": "uc_rebalancing_reform_after_data_load",
+      "check": "unreformed_after_reform:uc_LCWRA_element:2027",
+      "expected": 5259.26806640625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "a695bb7865a505dbbcb9c973b674a5666804e15838252c66bb7e84fa082388f0",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_rebalancing_reform_after_data_load",
+        "reform_sha256": "6f085d080cf6be93434e99ae984380f8ce87bdd6beab25c1abe4f1c9c8925a77",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:uc_LCWRA_element:2027:0",
+      "case_id": "uc_rebalancing_reform_after_data_load",
+      "check": "variable:uc_LCWRA_element:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "\u00a3500 x 12",
+      "reform_digest": "88900943775d8451bee562422107eca7016ee21042a516b295fe3c00bc9a7da9",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_rebalancing_reform_after_data_load",
+        "reform_sha256": "6f085d080cf6be93434e99ae984380f8ce87bdd6beab25c1abe4f1c9c8925a77",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 5259.26806640625,
+      "case_id": "uc_rebalancing_reform_after_data_load",
+      "check": "unreformed_after_reform:uc_LCWRA_element:2027",
+      "expected": 5259.26806640625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "88900943775d8451bee562422107eca7016ee21042a516b295fe3c00bc9a7da9",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "uc_rebalancing_reform_after_data_load",
+        "reform_sha256": "6f085d080cf6be93434e99ae984380f8ce87bdd6beab25c1abe4f1c9c8925a77",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "case_id": "uc_rebalancing_reform_after_data_load",
+      "check": null,
+      "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+      "reform_digest": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "the route does not run in-process",
+      "route": "api.sim_api",
+      "status": "unsupported"
+    },
+    {
+      "actual": 1260.0,
+      "assertion_id": "variable:marriage_allowance:2026:0",
+      "case_id": "structural_switch",
+      "check": "variable:marriage_allowance:2026",
+      "decision_id": null,
+      "expected": 1260.0,
+      "holds": null,
+      "oracle": "10% of the \u00a312,570 allowance, rounded up to \u00a310 (ITA 2007 s55B)",
+      "reform_digest": "d837da2e6fabde655a05b130322e0c29d06d5d493ad99183a699198dc5e1d4b2",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "structural_switch",
+        "reform_sha256": "e683e23a1d7da5038783f3a42f3208e01d34e9d873907ad213bacb1c9ad19a37",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "structural_switch",
+      "check": "baseline_isolation:marriage_allowance:2026",
+      "expected": 0.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "d837da2e6fabde655a05b130322e0c29d06d5d493ad99183a699198dc5e1d4b2",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "structural_switch",
+        "reform_sha256": "e683e23a1d7da5038783f3a42f3208e01d34e9d873907ad213bacb1c9ad19a37",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "structural_switch",
+      "check": "unreformed_after_reform:marriage_allowance:2026",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "d837da2e6fabde655a05b130322e0c29d06d5d493ad99183a699198dc5e1d4b2",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "structural_switch",
+        "reform_sha256": "e683e23a1d7da5038783f3a42f3208e01d34e9d873907ad213bacb1c9ad19a37",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "assertion_id": "variable:marriage_allowance:2026:0",
+      "case_id": "structural_switch",
+      "check": "variable:marriage_allowance:2026",
+      "decision_id": null,
+      "expected": 1260.0,
+      "holds": null,
+      "oracle": "10% of the \u00a312,570 allowance, rounded up to \u00a310 (ITA 2007 s55B)",
+      "reform_digest": "f0cad18ed901a4062c12dd721fa663915f3329fad55f54bbe407922de196d60d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "structural_switch",
+        "reform_sha256": "cb0da957a89c98b056fb93513eea1742af054428b69394dfa2a86997fefc3279",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "structural_switch",
+      "check": "baseline_isolation:marriage_allowance:2026",
+      "expected": 0.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "f0cad18ed901a4062c12dd721fa663915f3329fad55f54bbe407922de196d60d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "structural_switch",
+        "reform_sha256": "cb0da957a89c98b056fb93513eea1742af054428b69394dfa2a86997fefc3279",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "structural_switch",
+      "check": "unreformed_after_reform:marriage_allowance:2026",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "f0cad18ed901a4062c12dd721fa663915f3329fad55f54bbe407922de196d60d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "structural_switch",
+        "reform_sha256": "cb0da957a89c98b056fb93513eea1742af054428b69394dfa2a86997fefc3279",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "assertion_id": "variable:marriage_allowance:2026:0",
+      "case_id": "structural_switch",
+      "check": "variable:marriage_allowance:2026",
+      "decision_id": null,
+      "expected": 1260.0,
+      "holds": null,
+      "oracle": "10% of the \u00a312,570 allowance, rounded up to \u00a310 (ITA 2007 s55B)",
+      "reform_digest": "19b28590adbd8d9b80069a5504380f0226f0a1e5b1b2f73f5ba411fc562d7742",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "structural_switch",
+      "check": "baseline_isolation:marriage_allowance:2026",
+      "expected": 0.0,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "19b28590adbd8d9b80069a5504380f0226f0a1e5b1b2f73f5ba411fc562d7742",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "structural_switch",
+      "check": "unreformed_after_reform:marriage_allowance:2026",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "19b28590adbd8d9b80069a5504380f0226f0a1e5b1b2f73f5ba411fc562d7742",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "assertion_id": "variable:marriage_allowance:2026:0",
+      "case_id": "structural_switch",
+      "check": "variable:marriage_allowance:2026",
+      "decision_id": null,
+      "expected": 1260.0,
+      "holds": null,
+      "oracle": "10% of the \u00a312,570 allowance, rounded up to \u00a310 (ITA 2007 s55B)",
+      "reform_digest": "f76cc80028ce85017f3730a4d7db21c8a69f7b68bae0123c0aed45df087b1393",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "structural_switch",
+        "reform_sha256": "af729310839cbeb6c27fe6d8934b8dbddd37f6d9078ee45d8381a33ee72b568e",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "structural_switch",
+      "check": "unreformed_after_reform:marriage_allowance:2026",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "f76cc80028ce85017f3730a4d7db21c8a69f7b68bae0123c0aed45df087b1393",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "structural_switch",
+        "reform_sha256": "af729310839cbeb6c27fe6d8934b8dbddd37f6d9078ee45d8381a33ee72b568e",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 1260.0,
+      "assertion_id": "variable:marriage_allowance:2026:0",
+      "case_id": "structural_switch",
+      "check": "variable:marriage_allowance:2026",
+      "decision_id": null,
+      "expected": 1260.0,
+      "holds": null,
+      "oracle": "10% of the \u00a312,570 allowance, rounded up to \u00a310 (ITA 2007 s55B)",
+      "reform_digest": "6e7e98694b32def0a541b52e0abb79b1cdcfc1e50a70784147d24a17e652ffe0",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "structural_switch",
+        "reform_sha256": "af729310839cbeb6c27fe6d8934b8dbddd37f6d9078ee45d8381a33ee72b568e",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0,
+      "case_id": "structural_switch",
+      "check": "unreformed_after_reform:marriage_allowance:2026",
+      "expected": 0.0,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "6e7e98694b32def0a541b52e0abb79b1cdcfc1e50a70784147d24a17e652ffe0",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "structural_switch",
+        "reform_sha256": "af729310839cbeb6c27fe6d8934b8dbddd37f6d9078ee45d8381a33ee72b568e",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "case_id": "structural_switch",
+      "check": null,
+      "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+      "reform_digest": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "the route does not run in-process",
+      "route": "api.sim_api",
+      "status": "unsupported"
+    },
+    {
+      "actual": 5263.158203125,
+      "assertion_id": "baseline_equals:baseline_vat:2026:0",
+      "case_id": "baseline_vat_is_current_law",
+      "check": "baseline_equals:baseline_vat:2026",
+      "decision_id": null,
+      "expected": 5263.158203125,
+      "holds": null,
+      "oracle": "baseline_vat is the unreformed vat",
+      "reform_digest": "062a65e94c93f043b9ff9322b07152c930d1e1cb375237eb2375343a9e470e73",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "99683588e59cc7c52116b57f49a1f511b4598e73444272ec65538ef34e37de3a",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 526.3154296875,
+      "assertion_id": "change:vat:2026:1",
+      "case_id": "baseline_vat_is_current_law",
+      "check": "change:vat:2026",
+      "decision_id": null,
+      "expected": 526.3157894736839,
+      "holds": null,
+      "oracle": "\u00a310,000 of full-rate consumption x (22% - current rate), over the share of consumption the microdata covers; the current rate and coverage are read from current law",
+      "reform_digest": "062a65e94c93f043b9ff9322b07152c930d1e1cb375237eb2375343a9e470e73",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "99683588e59cc7c52116b57f49a1f511b4598e73444272ec65538ef34e37de3a",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5263.158203125,
+      "case_id": "baseline_vat_is_current_law",
+      "check": "baseline_isolation:baseline_vat:2026",
+      "expected": 5263.158203125,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "062a65e94c93f043b9ff9322b07152c930d1e1cb375237eb2375343a9e470e73",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "99683588e59cc7c52116b57f49a1f511b4598e73444272ec65538ef34e37de3a",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5263.158203125,
+      "case_id": "baseline_vat_is_current_law",
+      "check": "baseline_isolation:vat:2026",
+      "expected": 5263.158203125,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "062a65e94c93f043b9ff9322b07152c930d1e1cb375237eb2375343a9e470e73",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "99683588e59cc7c52116b57f49a1f511b4598e73444272ec65538ef34e37de3a",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5263.158203125,
+      "case_id": "baseline_vat_is_current_law",
+      "check": "unreformed_after_reform:baseline_vat:2026",
+      "expected": 5263.158203125,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "062a65e94c93f043b9ff9322b07152c930d1e1cb375237eb2375343a9e470e73",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "99683588e59cc7c52116b57f49a1f511b4598e73444272ec65538ef34e37de3a",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5263.158203125,
+      "case_id": "baseline_vat_is_current_law",
+      "check": "unreformed_after_reform:vat:2026",
+      "expected": 5263.158203125,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "062a65e94c93f043b9ff9322b07152c930d1e1cb375237eb2375343a9e470e73",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "99683588e59cc7c52116b57f49a1f511b4598e73444272ec65538ef34e37de3a",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5263.158203125,
+      "assertion_id": "baseline_equals:baseline_vat:2026:0",
+      "case_id": "baseline_vat_is_current_law",
+      "check": "baseline_equals:baseline_vat:2026",
+      "decision_id": null,
+      "expected": 5263.158203125,
+      "holds": null,
+      "oracle": "baseline_vat is the unreformed vat",
+      "reform_digest": "8a1e1a66ee167c2dc554452d46828576b58159ed3d3276ffa1723f417d5ade0d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "6caf1b1b8bce09446ab445fca925df3df2f2f90dc359e64f85fd5bae417d6ba4",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 526.3154296875,
+      "assertion_id": "change:vat:2026:1",
+      "case_id": "baseline_vat_is_current_law",
+      "check": "change:vat:2026",
+      "decision_id": null,
+      "expected": 526.3157894736839,
+      "holds": null,
+      "oracle": "\u00a310,000 of full-rate consumption x (22% - current rate), over the share of consumption the microdata covers; the current rate and coverage are read from current law",
+      "reform_digest": "8a1e1a66ee167c2dc554452d46828576b58159ed3d3276ffa1723f417d5ade0d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "6caf1b1b8bce09446ab445fca925df3df2f2f90dc359e64f85fd5bae417d6ba4",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5263.158203125,
+      "case_id": "baseline_vat_is_current_law",
+      "check": "baseline_isolation:baseline_vat:2026",
+      "expected": 5263.158203125,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "8a1e1a66ee167c2dc554452d46828576b58159ed3d3276ffa1723f417d5ade0d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "6caf1b1b8bce09446ab445fca925df3df2f2f90dc359e64f85fd5bae417d6ba4",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5263.158203125,
+      "case_id": "baseline_vat_is_current_law",
+      "check": "baseline_isolation:vat:2026",
+      "expected": 5263.158203125,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "8a1e1a66ee167c2dc554452d46828576b58159ed3d3276ffa1723f417d5ade0d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "6caf1b1b8bce09446ab445fca925df3df2f2f90dc359e64f85fd5bae417d6ba4",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5263.158203125,
+      "case_id": "baseline_vat_is_current_law",
+      "check": "unreformed_after_reform:baseline_vat:2026",
+      "expected": 5263.158203125,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "8a1e1a66ee167c2dc554452d46828576b58159ed3d3276ffa1723f417d5ade0d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "6caf1b1b8bce09446ab445fca925df3df2f2f90dc359e64f85fd5bae417d6ba4",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5263.158203125,
+      "case_id": "baseline_vat_is_current_law",
+      "check": "unreformed_after_reform:vat:2026",
+      "expected": 5263.158203125,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "8a1e1a66ee167c2dc554452d46828576b58159ed3d3276ffa1723f417d5ade0d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "6caf1b1b8bce09446ab445fca925df3df2f2f90dc359e64f85fd5bae417d6ba4",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 5263.158203125,
+      "assertion_id": "baseline_equals:baseline_vat:2026:0",
+      "case_id": "baseline_vat_is_current_law",
+      "check": "baseline_equals:baseline_vat:2026",
+      "decision_id": null,
+      "expected": 5263.158203125,
+      "holds": null,
+      "oracle": "baseline_vat is the unreformed vat",
+      "reform_digest": "8df662d00e257aeda318ef34432e8de4ab0474fcd093ab9afd16e5fd97c4f096",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 526.3154296875,
+      "assertion_id": "change:vat:2026:1",
+      "case_id": "baseline_vat_is_current_law",
+      "check": "change:vat:2026",
+      "decision_id": null,
+      "expected": 526.3157894736839,
+      "holds": null,
+      "oracle": "\u00a310,000 of full-rate consumption x (22% - current rate), over the share of consumption the microdata covers; the current rate and coverage are read from current law",
+      "reform_digest": "8df662d00e257aeda318ef34432e8de4ab0474fcd093ab9afd16e5fd97c4f096",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5263.158203125,
+      "case_id": "baseline_vat_is_current_law",
+      "check": "baseline_isolation:baseline_vat:2026",
+      "expected": 5263.158203125,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "8df662d00e257aeda318ef34432e8de4ab0474fcd093ab9afd16e5fd97c4f096",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5263.158203125,
+      "case_id": "baseline_vat_is_current_law",
+      "check": "baseline_isolation:vat:2026",
+      "expected": 5263.158203125,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "8df662d00e257aeda318ef34432e8de4ab0474fcd093ab9afd16e5fd97c4f096",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5263.158203125,
+      "case_id": "baseline_vat_is_current_law",
+      "check": "unreformed_after_reform:baseline_vat:2026",
+      "expected": 5263.158203125,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "8df662d00e257aeda318ef34432e8de4ab0474fcd093ab9afd16e5fd97c4f096",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5263.158203125,
+      "case_id": "baseline_vat_is_current_law",
+      "check": "unreformed_after_reform:vat:2026",
+      "expected": 5263.158203125,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "8df662d00e257aeda318ef34432e8de4ab0474fcd093ab9afd16e5fd97c4f096",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 5263.158203125,
+      "assertion_id": "baseline_equals:baseline_vat:2026:0",
+      "case_id": "baseline_vat_is_current_law",
+      "check": "baseline_equals:baseline_vat:2026",
+      "decision_id": null,
+      "expected": 5263.158203125,
+      "holds": null,
+      "oracle": "baseline_vat is the unreformed vat",
+      "reform_digest": "ee8e8cffbe252201a18cc7581c191e14f1b0269bfcfaf9d48cde191e9b2aaae9",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "0e57f240e31dc177dae06b735fd47b05f9c95894a049bf2816d3ae7f6114bb47",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 526.3154296875,
+      "assertion_id": "change:vat:2026:1",
+      "case_id": "baseline_vat_is_current_law",
+      "check": "change:vat:2026",
+      "decision_id": null,
+      "expected": 526.3157894736839,
+      "holds": null,
+      "oracle": "\u00a310,000 of full-rate consumption x (22% - current rate), over the share of consumption the microdata covers; the current rate and coverage are read from current law",
+      "reform_digest": "ee8e8cffbe252201a18cc7581c191e14f1b0269bfcfaf9d48cde191e9b2aaae9",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "0e57f240e31dc177dae06b735fd47b05f9c95894a049bf2816d3ae7f6114bb47",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 5263.158203125,
+      "case_id": "baseline_vat_is_current_law",
+      "check": "unreformed_after_reform:baseline_vat:2026",
+      "expected": 5263.158203125,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "ee8e8cffbe252201a18cc7581c191e14f1b0269bfcfaf9d48cde191e9b2aaae9",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "0e57f240e31dc177dae06b735fd47b05f9c95894a049bf2816d3ae7f6114bb47",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 5263.158203125,
+      "case_id": "baseline_vat_is_current_law",
+      "check": "unreformed_after_reform:vat:2026",
+      "expected": 5263.158203125,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "ee8e8cffbe252201a18cc7581c191e14f1b0269bfcfaf9d48cde191e9b2aaae9",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "0e57f240e31dc177dae06b735fd47b05f9c95894a049bf2816d3ae7f6114bb47",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "baseline_equals:baseline_vat:2026:0",
+      "case_id": "baseline_vat_is_current_law",
+      "check": "baseline_equals:baseline_vat:2026",
+      "reason": "the policy route's output dataset holds the reformed run only",
+      "reform_digest": "228c864948e49f1bafb1020ecd1a6a6aab050d94c43bd4f60144d4d6074d7626",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "0e57f240e31dc177dae06b735fd47b05f9c95894a049bf2816d3ae7f6114bb47",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "actual": 526.3154296875,
+      "assertion_id": "change:vat:2026:1",
+      "case_id": "baseline_vat_is_current_law",
+      "check": "change:vat:2026",
+      "decision_id": null,
+      "expected": 526.3157894736839,
+      "holds": null,
+      "oracle": "\u00a310,000 of full-rate consumption x (22% - current rate), over the share of consumption the microdata covers; the current rate and coverage are read from current law",
+      "reform_digest": "228c864948e49f1bafb1020ecd1a6a6aab050d94c43bd4f60144d4d6074d7626",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "0e57f240e31dc177dae06b735fd47b05f9c95894a049bf2816d3ae7f6114bb47",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 5263.158203125,
+      "case_id": "baseline_vat_is_current_law",
+      "check": "unreformed_after_reform:vat:2026",
+      "expected": 5263.158203125,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "228c864948e49f1bafb1020ecd1a6a6aab050d94c43bd4f60144d4d6074d7626",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "baseline_vat_is_current_law",
+        "reform_sha256": "0e57f240e31dc177dae06b735fd47b05f9c95894a049bf2816d3ae7f6114bb47",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "case_id": "baseline_vat_is_current_law",
+      "check": null,
+      "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+      "reform_digest": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "the route does not run in-process",
+      "route": "api.sim_api",
+      "status": "unsupported"
+    },
+    {
+      "actual": 0.0875,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2026:0",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2026",
+      "decision_id": null,
+      "expected": 0.0875,
+      "holds": null,
+      "oracle": "set by the modifier",
+      "reform_digest": "5c5e53ff74dc04bfc360ff580ca1013acb39023cef9edc4ad458d73bebe035d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0875,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2027:1",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.0875,
+      "holds": null,
+      "oracle": "set by the modifier",
+      "reform_digest": "5c5e53ff74dc04bfc360ff580ca1013acb39023cef9edc4ad458d73bebe035d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026:2",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "5c5e53ff74dc04bfc360ff580ca1013acb39023cef9edc4ad458d73bebe035d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.21,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:3",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.21,
+      "holds": null,
+      "oracle": "in force on 30 April 2027",
+      "reform_digest": "5c5e53ff74dc04bfc360ff580ca1013acb39023cef9edc4ad458d73bebe035d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": -190.0,
+      "assertion_id": "change:income_tax:2026:4",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "change:income_tax:2026",
+      "decision_id": null,
+      "expected": -190.00000000000003,
+      "holds": null,
+      "oracle": "(\u00a310,000 of dividends - the dividend allowance) x (8.75% - current dividend rate), all within the basic rate band",
+      "reform_digest": "5c5e53ff74dc04bfc360ff580ca1013acb39023cef9edc4ad458d73bebe035d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": -15.7001953125,
+      "assertion_id": "change:income_tax:2027:5",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "change:income_tax:2027",
+      "decision_id": null,
+      "expected": -15.700000000000358,
+      "holds": null,
+      "oracle": "(\u00a310,000 of dividends - the dividend allowance) x (8.75% - current dividend rate), all within the basic rate band, plus (30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "5c5e53ff74dc04bfc360ff580ca1013acb39023cef9edc4ad458d73bebe035d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "baseline_isolation:income_tax:2026",
+      "expected": 4507.25,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "5c5e53ff74dc04bfc360ff580ca1013acb39023cef9edc4ad458d73bebe035d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "baseline_isolation:income_tax:2027",
+      "expected": 4507.25,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "5c5e53ff74dc04bfc360ff580ca1013acb39023cef9edc4ad458d73bebe035d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.1075,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.dividends[0].rate:2026",
+      "expected": 0.1075,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "5c5e53ff74dc04bfc360ff580ca1013acb39023cef9edc4ad458d73bebe035d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.1075,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.dividends[0].rate:2027",
+      "expected": 0.1075,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "5c5e53ff74dc04bfc360ff580ca1013acb39023cef9edc4ad458d73bebe035d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "5c5e53ff74dc04bfc360ff580ca1013acb39023cef9edc4ad458d73bebe035d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "5c5e53ff74dc04bfc360ff580ca1013acb39023cef9edc4ad458d73bebe035d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:income_tax:2026",
+      "expected": 4507.25,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "5c5e53ff74dc04bfc360ff580ca1013acb39023cef9edc4ad458d73bebe035d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:income_tax:2027",
+      "expected": 4507.25,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "5c5e53ff74dc04bfc360ff580ca1013acb39023cef9edc4ad458d73bebe035d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0875,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2026:0",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2026",
+      "decision_id": null,
+      "expected": 0.0875,
+      "holds": null,
+      "oracle": "set by the modifier",
+      "reform_digest": "05701fb7bdc19fc311b70302afe7979c1f8e5b17b9484e76d3906b11d207de83",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0875,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2027:1",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.0875,
+      "holds": null,
+      "oracle": "set by the modifier",
+      "reform_digest": "05701fb7bdc19fc311b70302afe7979c1f8e5b17b9484e76d3906b11d207de83",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026:2",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "05701fb7bdc19fc311b70302afe7979c1f8e5b17b9484e76d3906b11d207de83",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.21,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:3",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.21,
+      "holds": null,
+      "oracle": "in force on 30 April 2027",
+      "reform_digest": "05701fb7bdc19fc311b70302afe7979c1f8e5b17b9484e76d3906b11d207de83",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": -190.0,
+      "assertion_id": "change:income_tax:2026:4",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "change:income_tax:2026",
+      "decision_id": null,
+      "expected": -190.00000000000003,
+      "holds": null,
+      "oracle": "(\u00a310,000 of dividends - the dividend allowance) x (8.75% - current dividend rate), all within the basic rate band",
+      "reform_digest": "05701fb7bdc19fc311b70302afe7979c1f8e5b17b9484e76d3906b11d207de83",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": -15.7001953125,
+      "assertion_id": "change:income_tax:2027:5",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "change:income_tax:2027",
+      "decision_id": null,
+      "expected": -15.700000000000358,
+      "holds": null,
+      "oracle": "(\u00a310,000 of dividends - the dividend allowance) x (8.75% - current dividend rate), all within the basic rate band, plus (30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "05701fb7bdc19fc311b70302afe7979c1f8e5b17b9484e76d3906b11d207de83",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "baseline_isolation:income_tax:2026",
+      "expected": 4507.25,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "05701fb7bdc19fc311b70302afe7979c1f8e5b17b9484e76d3906b11d207de83",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "baseline_isolation:income_tax:2027",
+      "expected": 4507.25,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "05701fb7bdc19fc311b70302afe7979c1f8e5b17b9484e76d3906b11d207de83",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.1075,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.dividends[0].rate:2026",
+      "expected": 0.1075,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "05701fb7bdc19fc311b70302afe7979c1f8e5b17b9484e76d3906b11d207de83",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.1075,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.dividends[0].rate:2027",
+      "expected": 0.1075,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "05701fb7bdc19fc311b70302afe7979c1f8e5b17b9484e76d3906b11d207de83",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "05701fb7bdc19fc311b70302afe7979c1f8e5b17b9484e76d3906b11d207de83",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "05701fb7bdc19fc311b70302afe7979c1f8e5b17b9484e76d3906b11d207de83",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:income_tax:2026",
+      "expected": 4507.25,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "05701fb7bdc19fc311b70302afe7979c1f8e5b17b9484e76d3906b11d207de83",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:income_tax:2027",
+      "expected": 4507.25,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "05701fb7bdc19fc311b70302afe7979c1f8e5b17b9484e76d3906b11d207de83",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0875,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2026:0",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2026",
+      "decision_id": null,
+      "expected": 0.0875,
+      "holds": null,
+      "oracle": "set by the modifier",
+      "reform_digest": "e6aba941ed6d2857584bcb6323f8e4186dc97005a39b1a9f05458858523ba545",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0875,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2027:1",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.0875,
+      "holds": null,
+      "oracle": "set by the modifier",
+      "reform_digest": "e6aba941ed6d2857584bcb6323f8e4186dc97005a39b1a9f05458858523ba545",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026:2",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "e6aba941ed6d2857584bcb6323f8e4186dc97005a39b1a9f05458858523ba545",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.21,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:3",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.21,
+      "holds": null,
+      "oracle": "in force on 30 April 2027",
+      "reform_digest": "e6aba941ed6d2857584bcb6323f8e4186dc97005a39b1a9f05458858523ba545",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": -190.0,
+      "assertion_id": "change:income_tax:2026:4",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "change:income_tax:2026",
+      "decision_id": null,
+      "expected": -190.00000000000003,
+      "holds": null,
+      "oracle": "(\u00a310,000 of dividends - the dividend allowance) x (8.75% - current dividend rate), all within the basic rate band",
+      "reform_digest": "e6aba941ed6d2857584bcb6323f8e4186dc97005a39b1a9f05458858523ba545",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": -15.7001953125,
+      "assertion_id": "change:income_tax:2027:5",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "change:income_tax:2027",
+      "decision_id": null,
+      "expected": -15.700000000000358,
+      "holds": null,
+      "oracle": "(\u00a310,000 of dividends - the dividend allowance) x (8.75% - current dividend rate), all within the basic rate band, plus (30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "e6aba941ed6d2857584bcb6323f8e4186dc97005a39b1a9f05458858523ba545",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "baseline_isolation:income_tax:2026",
+      "expected": 4507.25,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "e6aba941ed6d2857584bcb6323f8e4186dc97005a39b1a9f05458858523ba545",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "baseline_isolation:income_tax:2027",
+      "expected": 4507.25,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "e6aba941ed6d2857584bcb6323f8e4186dc97005a39b1a9f05458858523ba545",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.1075,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.dividends[0].rate:2026",
+      "expected": 0.1075,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e6aba941ed6d2857584bcb6323f8e4186dc97005a39b1a9f05458858523ba545",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.1075,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.dividends[0].rate:2027",
+      "expected": 0.1075,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e6aba941ed6d2857584bcb6323f8e4186dc97005a39b1a9f05458858523ba545",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e6aba941ed6d2857584bcb6323f8e4186dc97005a39b1a9f05458858523ba545",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e6aba941ed6d2857584bcb6323f8e4186dc97005a39b1a9f05458858523ba545",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:income_tax:2026",
+      "expected": 4507.25,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e6aba941ed6d2857584bcb6323f8e4186dc97005a39b1a9f05458858523ba545",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:income_tax:2027",
+      "expected": 4507.25,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "e6aba941ed6d2857584bcb6323f8e4186dc97005a39b1a9f05458858523ba545",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2026:0",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2026",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "fdd130c561e213b17840d484c320191f464d341cf29bb534f03aa9a846d143d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2027:1",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "fdd130c561e213b17840d484c320191f464d341cf29bb534f03aa9a846d143d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026:2",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "fdd130c561e213b17840d484c320191f464d341cf29bb534f03aa9a846d143d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:3",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "fdd130c561e213b17840d484c320191f464d341cf29bb534f03aa9a846d143d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "actual": -190.0,
+      "assertion_id": "change:income_tax:2026:4",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "change:income_tax:2026",
+      "decision_id": null,
+      "expected": -190.00000000000003,
+      "holds": null,
+      "oracle": "(\u00a310,000 of dividends - the dividend allowance) x (8.75% - current dividend rate), all within the basic rate band",
+      "reform_digest": "fdd130c561e213b17840d484c320191f464d341cf29bb534f03aa9a846d143d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": -15.7001953125,
+      "assertion_id": "change:income_tax:2027:5",
+      "case_id": "modifier_then_parameter_changes",
+      "check": "change:income_tax:2027",
+      "decision_id": null,
+      "expected": -15.700000000000358,
+      "holds": null,
+      "oracle": "(\u00a310,000 of dividends - the dividend allowance) x (8.75% - current dividend rate), all within the basic rate band, plus (30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "fdd130c561e213b17840d484c320191f464d341cf29bb534f03aa9a846d143d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:income_tax:2026",
+      "expected": 4507.25,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fdd130c561e213b17840d484c320191f464d341cf29bb534f03aa9a846d143d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "modifier_then_parameter_changes",
+      "check": "unreformed_after_reform:income_tax:2027",
+      "expected": 4507.25,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fdd130c561e213b17840d484c320191f464d341cf29bb534f03aa9a846d143d5",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0875,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2026:0",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2026",
+      "decision_id": null,
+      "expected": 0.0875,
+      "holds": null,
+      "oracle": "set by the modifier",
+      "reform_digest": "9fd82d1d38614616b8ee42850e07bc6bbd018da3b7007014609714017d39a3d4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0875,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2027:1",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.0875,
+      "holds": null,
+      "oracle": "set by the modifier",
+      "reform_digest": "9fd82d1d38614616b8ee42850e07bc6bbd018da3b7007014609714017d39a3d4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026:2",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "9fd82d1d38614616b8ee42850e07bc6bbd018da3b7007014609714017d39a3d4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.21,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:3",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.21,
+      "holds": null,
+      "oracle": "in force on 30 April 2027",
+      "reform_digest": "9fd82d1d38614616b8ee42850e07bc6bbd018da3b7007014609714017d39a3d4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": -190.0,
+      "assertion_id": "change:income_tax:2026:4",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "change:income_tax:2026",
+      "decision_id": null,
+      "expected": -190.00000000000003,
+      "holds": null,
+      "oracle": "(\u00a310,000 of dividends - the dividend allowance) x (8.75% - current dividend rate), all within the basic rate band",
+      "reform_digest": "9fd82d1d38614616b8ee42850e07bc6bbd018da3b7007014609714017d39a3d4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": -15.7001953125,
+      "assertion_id": "change:income_tax:2027:5",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "change:income_tax:2027",
+      "decision_id": null,
+      "expected": -15.700000000000358,
+      "holds": null,
+      "oracle": "(\u00a310,000 of dividends - the dividend allowance) x (8.75% - current dividend rate), all within the basic rate band, plus (30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "9fd82d1d38614616b8ee42850e07bc6bbd018da3b7007014609714017d39a3d4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "baseline_isolation:income_tax:2026",
+      "expected": 4507.25,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "9fd82d1d38614616b8ee42850e07bc6bbd018da3b7007014609714017d39a3d4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "baseline_isolation:income_tax:2027",
+      "expected": 4507.25,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "9fd82d1d38614616b8ee42850e07bc6bbd018da3b7007014609714017d39a3d4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.1075,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.dividends[0].rate:2026",
+      "expected": 0.1075,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "9fd82d1d38614616b8ee42850e07bc6bbd018da3b7007014609714017d39a3d4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.1075,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.dividends[0].rate:2027",
+      "expected": 0.1075,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "9fd82d1d38614616b8ee42850e07bc6bbd018da3b7007014609714017d39a3d4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "9fd82d1d38614616b8ee42850e07bc6bbd018da3b7007014609714017d39a3d4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "9fd82d1d38614616b8ee42850e07bc6bbd018da3b7007014609714017d39a3d4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:income_tax:2026",
+      "expected": 4507.25,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "9fd82d1d38614616b8ee42850e07bc6bbd018da3b7007014609714017d39a3d4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:income_tax:2027",
+      "expected": 4507.25,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "9fd82d1d38614616b8ee42850e07bc6bbd018da3b7007014609714017d39a3d4",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0875,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2026:0",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2026",
+      "decision_id": null,
+      "expected": 0.0875,
+      "holds": null,
+      "oracle": "set by the modifier",
+      "reform_digest": "fcaf08431cabc68b5e0b6983a816afaa3482a7d32ef9f23deaca75753b191249",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0875,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2027:1",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.0875,
+      "holds": null,
+      "oracle": "set by the modifier",
+      "reform_digest": "fcaf08431cabc68b5e0b6983a816afaa3482a7d32ef9f23deaca75753b191249",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026:2",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "fcaf08431cabc68b5e0b6983a816afaa3482a7d32ef9f23deaca75753b191249",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.21,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:3",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.21,
+      "holds": null,
+      "oracle": "in force on 30 April 2027",
+      "reform_digest": "fcaf08431cabc68b5e0b6983a816afaa3482a7d32ef9f23deaca75753b191249",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": -190.0,
+      "assertion_id": "change:income_tax:2026:4",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "change:income_tax:2026",
+      "decision_id": null,
+      "expected": -190.00000000000003,
+      "holds": null,
+      "oracle": "(\u00a310,000 of dividends - the dividend allowance) x (8.75% - current dividend rate), all within the basic rate band",
+      "reform_digest": "fcaf08431cabc68b5e0b6983a816afaa3482a7d32ef9f23deaca75753b191249",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": -15.7001953125,
+      "assertion_id": "change:income_tax:2027:5",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "change:income_tax:2027",
+      "decision_id": null,
+      "expected": -15.700000000000358,
+      "holds": null,
+      "oracle": "(\u00a310,000 of dividends - the dividend allowance) x (8.75% - current dividend rate), all within the basic rate band, plus (30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "fcaf08431cabc68b5e0b6983a816afaa3482a7d32ef9f23deaca75753b191249",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "baseline_isolation:income_tax:2026",
+      "expected": 4507.25,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "fcaf08431cabc68b5e0b6983a816afaa3482a7d32ef9f23deaca75753b191249",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "baseline_isolation:income_tax:2027",
+      "expected": 4507.25,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "fcaf08431cabc68b5e0b6983a816afaa3482a7d32ef9f23deaca75753b191249",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.1075,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.dividends[0].rate:2026",
+      "expected": 0.1075,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fcaf08431cabc68b5e0b6983a816afaa3482a7d32ef9f23deaca75753b191249",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.1075,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.dividends[0].rate:2027",
+      "expected": 0.1075,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fcaf08431cabc68b5e0b6983a816afaa3482a7d32ef9f23deaca75753b191249",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fcaf08431cabc68b5e0b6983a816afaa3482a7d32ef9f23deaca75753b191249",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fcaf08431cabc68b5e0b6983a816afaa3482a7d32ef9f23deaca75753b191249",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:income_tax:2026",
+      "expected": 4507.25,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fcaf08431cabc68b5e0b6983a816afaa3482a7d32ef9f23deaca75753b191249",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:income_tax:2027",
+      "expected": 4507.25,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "fcaf08431cabc68b5e0b6983a816afaa3482a7d32ef9f23deaca75753b191249",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0875,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2026:0",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2026",
+      "decision_id": null,
+      "expected": 0.0875,
+      "holds": null,
+      "oracle": "set by the modifier",
+      "reform_digest": "8e7554729292ab5cb608f34d2be15d8619eefa8f492ca83e2d7c93b41e2ca3dd",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.0875,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2027:1",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.0875,
+      "holds": null,
+      "oracle": "set by the modifier",
+      "reform_digest": "8e7554729292ab5cb608f34d2be15d8619eefa8f492ca83e2d7c93b41e2ca3dd",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026:2",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "before the change",
+      "reform_digest": "8e7554729292ab5cb608f34d2be15d8619eefa8f492ca83e2d7c93b41e2ca3dd",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.21,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:3",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.21,
+      "holds": null,
+      "oracle": "in force on 30 April 2027",
+      "reform_digest": "8e7554729292ab5cb608f34d2be15d8619eefa8f492ca83e2d7c93b41e2ca3dd",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": -190.0,
+      "assertion_id": "change:income_tax:2026:4",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "change:income_tax:2026",
+      "decision_id": null,
+      "expected": -190.00000000000003,
+      "holds": null,
+      "oracle": "(\u00a310,000 of dividends - the dividend allowance) x (8.75% - current dividend rate), all within the basic rate band",
+      "reform_digest": "8e7554729292ab5cb608f34d2be15d8619eefa8f492ca83e2d7c93b41e2ca3dd",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": -15.7001953125,
+      "assertion_id": "change:income_tax:2027:5",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "change:income_tax:2027",
+      "decision_id": null,
+      "expected": -15.700000000000358,
+      "holds": null,
+      "oracle": "(\u00a310,000 of dividends - the dividend allowance) x (8.75% - current dividend rate), all within the basic rate band, plus (30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "8e7554729292ab5cb608f34d2be15d8619eefa8f492ca83e2d7c93b41e2ca3dd",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "baseline_isolation:income_tax:2026",
+      "expected": 4507.25,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "8e7554729292ab5cb608f34d2be15d8619eefa8f492ca83e2d7c93b41e2ca3dd",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "baseline_isolation:income_tax:2027",
+      "expected": 4507.25,
+      "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+      "reform_digest": "8e7554729292ab5cb608f34d2be15d8619eefa8f492ca83e2d7c93b41e2ca3dd",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.1075,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.dividends[0].rate:2026",
+      "expected": 0.1075,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "8e7554729292ab5cb608f34d2be15d8619eefa8f492ca83e2d7c93b41e2ca3dd",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.1075,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.dividends[0].rate:2027",
+      "expected": 0.1075,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "8e7554729292ab5cb608f34d2be15d8619eefa8f492ca83e2d7c93b41e2ca3dd",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "8e7554729292ab5cb608f34d2be15d8619eefa8f492ca83e2d7c93b41e2ca3dd",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "8e7554729292ab5cb608f34d2be15d8619eefa8f492ca83e2d7c93b41e2ca3dd",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:income_tax:2026",
+      "expected": 4507.25,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "8e7554729292ab5cb608f34d2be15d8619eefa8f492ca83e2d7c93b41e2ca3dd",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:income_tax:2027",
+      "expected": 4507.25,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "8e7554729292ab5cb608f34d2be15d8619eefa8f492ca83e2d7c93b41e2ca3dd",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2026:0",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2026",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "43a019779739fe68e8b656937b0ddc64095111ef68da072075d4212c17e66faf",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2027:1",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "parameter:gov.hmrc.income_tax.rates.dividends[0].rate:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "43a019779739fe68e8b656937b0ddc64095111ef68da072075d4212c17e66faf",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026:2",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "43a019779739fe68e8b656937b0ddc64095111ef68da072075d4212c17e66faf",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:3",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "reason": "this route exposes outputs, not parameter values",
+      "reform_digest": "43a019779739fe68e8b656937b0ddc64095111ef68da072075d4212c17e66faf",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "wrapper.policy",
+      "status": "unsupported"
+    },
+    {
+      "actual": -190.0,
+      "assertion_id": "change:income_tax:2026:4",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "change:income_tax:2026",
+      "decision_id": null,
+      "expected": -190.00000000000003,
+      "holds": null,
+      "oracle": "(\u00a310,000 of dividends - the dividend allowance) x (8.75% - current dividend rate), all within the basic rate band",
+      "reform_digest": "43a019779739fe68e8b656937b0ddc64095111ef68da072075d4212c17e66faf",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": -15.7001953125,
+      "assertion_id": "change:income_tax:2027:5",
+      "case_id": "parameter_changes_then_modifier",
+      "check": "change:income_tax:2027",
+      "decision_id": null,
+      "expected": -15.700000000000358,
+      "holds": null,
+      "oracle": "(\u00a310,000 of dividends - the dividend allowance) x (8.75% - current dividend rate), all within the basic rate band, plus (30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+      "reform_digest": "43a019779739fe68e8b656937b0ddc64095111ef68da072075d4212c17e66faf",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:income_tax:2026",
+      "expected": 4507.25,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "43a019779739fe68e8b656937b0ddc64095111ef68da072075d4212c17e66faf",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 4507.25,
+      "case_id": "parameter_changes_then_modifier",
+      "check": "unreformed_after_reform:income_tax:2027",
+      "expected": 4507.25,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "43a019779739fe68e8b656937b0ddc64095111ef68da072075d4212c17e66faf",
+      "reform_identity": null,
+      "reform_identity_unavailable": "the case's simulation modifier is a closure, which scenario_reform_identity refuses to bind",
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": "ValueError: Could not find the parameter gov.budget_2026.new_credit.amount (failed at budget_2026).",
+      "case_id": "unknown_path",
+      "check": "rejected",
+      "expected": "an error matching 'new_credit'",
+      "reform_digest": "8465ddfc32335f10bb9d23b050f0156a37c48e8a10dc35719d6b7e4f6d1a9238",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "unknown_path",
+        "reform_sha256": "a37b60f951358a9333d2ae2d68f9c50e5126fe06c7fa8d2b530f7e27ddd1e623",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": "ValueError: Could not find the parameter gov.budget_2026.new_credit.amount (failed at budget_2026).",
+      "case_id": "unknown_path",
+      "check": "rejected",
+      "expected": "an error matching 'new_credit'",
+      "reform_digest": "a72b9604c243f77845e1446379e85bd2eca8e0fa7c671956a26e05ecadc3c4a7",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "unknown_path",
+        "reform_sha256": "d2935e4ad6fd5c5e48e57c51bf59dfcadc6c969431ebe4d381fd4725580783db",
+        "representation": "policyengine-uk.scenario/v1"
+      },
+      "route": "country.scenario_parameter_changes",
+      "status": "pass"
+    },
+    {
+      "actual": "ValueError: Could not find the parameter gov.budget_2026.new_credit.amount (failed at budget_2026).",
+      "case_id": "unknown_path",
+      "check": "rejected",
+      "expected": "an error matching 'new_credit'",
+      "reform_digest": "32c326f8ca94bac9e220000e7f0fcf1a34bccd4947357e3819d419bb6ff193ea",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": "ValueError: Reform contains parameter paths not defined on policyengine-uk 2.130.1:\n  - 'gov.budget_2026.new_credit.amount'",
+      "case_id": "unknown_path",
+      "check": "rejected",
+      "expected": "an error matching 'new_credit'",
+      "reform_digest": "292d055c4ba7f1a326b9fa798441a0f6d2186c07206ce7d63177d829371ee335",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "unknown_path",
+        "reform_sha256": "6b1d9cc2b45966b2ef37950ad39d708644e376e9a203b158d004a73d0df66b85",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": "ValueError: Reform contains parameter paths not defined on policyengine-uk 2.130.1:\n  - 'gov.budget_2026.new_credit.amount'",
+      "case_id": "unknown_path",
+      "check": "rejected",
+      "expected": "an error matching 'new_credit'",
+      "reform_digest": "29a0b52eda9615814b814d0de2932d01d8d59430150dcbf9fbb2fcdf79d1fd8d",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "unknown_path",
+        "reform_sha256": "6b1d9cc2b45966b2ef37950ad39d708644e376e9a203b158d004a73d0df66b85",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "case_id": "unknown_path",
+      "check": null,
+      "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+      "reform_digest": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "the route does not run in-process",
+      "route": "api.sim_api",
+      "status": "unsupported"
+    },
+    {
+      "actual": 0.21,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:0",
+      "case_id": "reading_bare_year_dict",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.21,
+      "holds": null,
+      "oracle": "fiscal year 2027",
+      "reform_digest": "3314f6d0362de9643ce2022904e4fcbbe84297c25d36c37add61a878c7dc3e9c",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "reading_bare_year_dict",
+        "reform_sha256": "a17ad3d70916d16416c1181d6f7e83b43c6736908f321e0984abdef8df0fe81d",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028:1",
+      "case_id": "reading_bare_year_dict",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "that year only",
+      "reform_digest": "3314f6d0362de9643ce2022904e4fcbbe84297c25d36c37add61a878c7dc3e9c",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "reading_bare_year_dict",
+        "reform_sha256": "a17ad3d70916d16416c1181d6f7e83b43c6736908f321e0984abdef8df0fe81d",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "reading_bare_year_dict",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "3314f6d0362de9643ce2022904e4fcbbe84297c25d36c37add61a878c7dc3e9c",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "reading_bare_year_dict",
+        "reform_sha256": "a17ad3d70916d16416c1181d6f7e83b43c6736908f321e0984abdef8df0fe81d",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "reading_bare_year_dict",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "3314f6d0362de9643ce2022904e4fcbbe84297c25d36c37add61a878c7dc3e9c",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "reading_bare_year_dict",
+        "reform_sha256": "a17ad3d70916d16416c1181d6f7e83b43c6736908f321e0984abdef8df0fe81d",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026:0",
+      "case_id": "reading_bare_year_reform_from_dict",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "before 1 January 2027",
+      "reform_digest": "d19ea548203c2417e712543d47546f3fa48c741252ccd5b6bf1664d9a517d6b6",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.21,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:1",
+      "case_id": "reading_bare_year_reform_from_dict",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.21,
+      "holds": null,
+      "oracle": "from 1 January 2027",
+      "reform_digest": "d19ea548203c2417e712543d47546f3fa48c741252ccd5b6bf1664d9a517d6b6",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.21,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028:2",
+      "case_id": "reading_bare_year_reform_from_dict",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "decision_id": null,
+      "expected": 0.21,
+      "holds": null,
+      "oracle": "onwards",
+      "reform_digest": "d19ea548203c2417e712543d47546f3fa48c741252ccd5b6bf1664d9a517d6b6",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "reading_bare_year_reform_from_dict",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "d19ea548203c2417e712543d47546f3fa48c741252ccd5b6bf1664d9a517d6b6",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "reading_bare_year_reform_from_dict",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "d19ea548203c2417e712543d47546f3fa48c741252ccd5b6bf1664d9a517d6b6",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "reading_bare_year_reform_from_dict",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "d19ea548203c2417e712543d47546f3fa48c741252ccd5b6bf1664d9a517d6b6",
+      "reform_identity": null,
+      "reform_identity_unavailable": "receipt schema v1 has no representation for what country.reform_from_dict consumes",
+      "route": "country.reform_from_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027:0",
+      "case_id": "reading_single_day_dict",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "one day is not 30 April",
+      "reform_digest": "dec76eb1ea64e800caff37168afaaa26599bb03b533c169908d691c111cdbc36",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "reading_single_day_dict",
+        "reform_sha256": "f576ac6d74b8e3dc782eaeeb81b6254e76baa72546eac3ab2adec7a6052f9174",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "assertion_id": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028:1",
+      "case_id": "reading_single_day_dict",
+      "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "decision_id": null,
+      "expected": 0.2,
+      "holds": null,
+      "oracle": "one day only",
+      "reform_digest": "dec76eb1ea64e800caff37168afaaa26599bb03b533c169908d691c111cdbc36",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "reading_single_day_dict",
+        "reform_sha256": "f576ac6d74b8e3dc782eaeeb81b6254e76baa72546eac3ab2adec7a6052f9174",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "reading_single_day_dict",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "dec76eb1ea64e800caff37168afaaa26599bb03b533c169908d691c111cdbc36",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "reading_single_day_dict",
+        "reform_sha256": "f576ac6d74b8e3dc782eaeeb81b6254e76baa72546eac3ab2adec7a6052f9174",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 0.2,
+      "case_id": "reading_single_day_dict",
+      "check": "unreformed_after_reform:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+      "expected": 0.2,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "dec76eb1ea64e800caff37168afaaa26599bb03b533c169908d691c111cdbc36",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "reading_single_day_dict",
+        "reform_sha256": "f576ac6d74b8e3dc782eaeeb81b6254e76baa72546eac3ab2adec7a6052f9174",
+        "representation": "policyengine-uk.reform_dict/v1"
+      },
+      "route": "country.reform_dict",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:uc_standard_allowance:2027:0",
+      "case_id": "reading_effective_dates_reverse_order",
+      "check": "variable:uc_standard_allowance:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "\u00a3500 x 12",
+      "reform_digest": "56c7ad7d1ddf6185703a5f60d31f11a12272e1166c667996b484801f57dbebcd",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "reading_effective_dates_reverse_order",
+        "reform_sha256": "fa2c95313ac6daa569395bf1b5d8595d5399731158ad6b5132641f0e3eac983c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 6240.0,
+      "assertion_id": "variable:uc_standard_allowance:2028:1",
+      "case_id": "reading_effective_dates_reverse_order",
+      "check": "variable:uc_standard_allowance:2028",
+      "decision_id": null,
+      "expected": 6240.0,
+      "holds": null,
+      "oracle": "\u00a3520 x 12",
+      "reform_digest": "56c7ad7d1ddf6185703a5f60d31f11a12272e1166c667996b484801f57dbebcd",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "reading_effective_dates_reverse_order",
+        "reform_sha256": "fa2c95313ac6daa569395bf1b5d8595d5399731158ad6b5132641f0e3eac983c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "reading_effective_dates_reverse_order",
+      "check": "unreformed_after_reform:uc_standard_allowance:2027",
+      "expected": 5216.06103515625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "56c7ad7d1ddf6185703a5f60d31f11a12272e1166c667996b484801f57dbebcd",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "reading_effective_dates_reverse_order",
+        "reform_sha256": "fa2c95313ac6daa569395bf1b5d8595d5399731158ad6b5132641f0e3eac983c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "reading_effective_dates_reverse_order",
+      "check": "unreformed_after_reform:uc_standard_allowance:2028",
+      "expected": 5320.3740234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "56c7ad7d1ddf6185703a5f60d31f11a12272e1166c667996b484801f57dbebcd",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "reading_effective_dates_reverse_order",
+        "reform_sha256": "fa2c95313ac6daa569395bf1b5d8595d5399731158ad6b5132641f0e3eac983c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.household",
+      "status": "pass"
+    },
+    {
+      "actual": 6000.0,
+      "assertion_id": "variable:uc_standard_allowance:2027:0",
+      "case_id": "reading_effective_dates_reverse_order",
+      "check": "variable:uc_standard_allowance:2027",
+      "decision_id": null,
+      "expected": 6000.0,
+      "holds": null,
+      "oracle": "\u00a3500 x 12",
+      "reform_digest": "b09577ce022e4cc30b4702add142a1e3aa1ab438a27955d182bba894199cecd5",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "reading_effective_dates_reverse_order",
+        "reform_sha256": "fa2c95313ac6daa569395bf1b5d8595d5399731158ad6b5132641f0e3eac983c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 6240.0,
+      "assertion_id": "variable:uc_standard_allowance:2028:1",
+      "case_id": "reading_effective_dates_reverse_order",
+      "check": "variable:uc_standard_allowance:2028",
+      "decision_id": null,
+      "expected": 6240.0,
+      "holds": null,
+      "oracle": "\u00a3520 x 12",
+      "reform_digest": "b09577ce022e4cc30b4702add142a1e3aa1ab438a27955d182bba894199cecd5",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "reading_effective_dates_reverse_order",
+        "reform_sha256": "fa2c95313ac6daa569395bf1b5d8595d5399731158ad6b5132641f0e3eac983c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 5216.06103515625,
+      "case_id": "reading_effective_dates_reverse_order",
+      "check": "unreformed_after_reform:uc_standard_allowance:2027",
+      "expected": 5216.06103515625,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "b09577ce022e4cc30b4702add142a1e3aa1ab438a27955d182bba894199cecd5",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "reading_effective_dates_reverse_order",
+        "reform_sha256": "fa2c95313ac6daa569395bf1b5d8595d5399731158ad6b5132641f0e3eac983c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": 5320.3740234375,
+      "case_id": "reading_effective_dates_reverse_order",
+      "check": "unreformed_after_reform:uc_standard_allowance:2028",
+      "expected": 5320.3740234375,
+      "oracle": "an unreformed simulation built after the reform equals one built before it",
+      "reform_digest": "b09577ce022e4cc30b4702add142a1e3aa1ab438a27955d182bba894199cecd5",
+      "reform_identity": {
+        "binding": "content",
+        "kind": "reform",
+        "name": "reading_effective_dates_reverse_order",
+        "reform_sha256": "fa2c95313ac6daa569395bf1b5d8595d5399731158ad6b5132641f0e3eac983c",
+        "representation": "policyengine.reform_dict/v1"
+      },
+      "route": "wrapper.policy",
+      "status": "pass"
+    },
+    {
+      "actual": "unchanged",
+      "case_id": "module_parameters_unchanged",
+      "check": "every parameter's dated values and metadata, and every node's metadata and band order, in processed, projected, statutory, system",
+      "expected": "policyengine-uk's shared parameters as they were before the run",
+      "oracle": "the shared parameter state's values, read before any reform ran",
+      "reform_digest": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "a run-level row runs no reform",
+      "route": "all",
+      "status": "pass"
+    }
+  ],
+  "contract_id": "policyengine-uk/budget-reform-contract",
+  "contract_version": "1.1.0",
+  "dataset": {
+    "path": "",
+    "sha256": "056e80fd8e9151d78f432a25cde863340baee81304c6216a37fb575ce8563784"
+  },
+  "holds": true,
+  "max_examples": 20,
+  "population": [
+    {
+      "check_id": "population_baseline_isolation",
+      "detail": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "country.reform_dict",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    },
+    {
+      "check_id": "population_baseline_isolation",
+      "detail": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "country.scenario_parameter_changes",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    },
+    {
+      "check_id": "population_baseline_isolation",
+      "detail": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "country.reform_from_dict",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    },
+    {
+      "check_id": "population_baseline_isolation",
+      "detail": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "unreformed",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    },
+    {
+      "check_id": "population_query_order",
+      "detail": null,
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "unreformed",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    },
+    {
+      "check_id": "population_routes_agree",
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "country.reform_dict = country.scenario_parameter_changes",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    },
+    {
+      "check_id": "population_routes_agree",
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "country.reform_dict = country.reform_from_dict",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    },
+    {
+      "check_id": "population_april_rate_change",
+      "detail": {
+        "2026_unchanged": true,
+        "2027_people_with_higher_tax": 1093,
+        "2027_people_with_lower_tax": "fewer than 10",
+        "2027_total_change": 205573.0608100891
+      },
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "country.reform_dict",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    },
+    {
+      "check_id": "population_april_rate_change",
+      "detail": {
+        "2026_unchanged": true,
+        "2027_people_with_higher_tax": 1093,
+        "2027_people_with_lower_tax": "fewer than 10",
+        "2027_total_change": 205573.0608100891
+      },
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "country.scenario_parameter_changes",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    },
+    {
+      "check_id": "population_april_rate_change",
+      "detail": {
+        "2026_unchanged": true,
+        "2027_people_with_higher_tax": 1093,
+        "2027_people_with_lower_tax": "fewer than 10",
+        "2027_total_change": 205573.0608100891
+      },
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "country.reform_from_dict",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    },
+    {
+      "check_id": "population_finite_change_expires",
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "country.reform_dict",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    },
+    {
+      "check_id": "population_finite_change_expires",
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "country.scenario_parameter_changes",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    },
+    {
+      "check_id": "population_finite_change_expires",
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "country.reform_from_dict",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    },
+    {
+      "check_id": "population_uc_standard_allowance_plus_10",
+      "detail": {
+        "awards_lower": 0,
+        "payments_below_the_payable_floor": 0,
+        "payments_higher": 102,
+        "payments_lower": "fewer than 10",
+        "payments_not_award_less_deductions": 0,
+        "standard_allowance_scaled_10_percent": true,
+        "units_with_non_finite_values": 0
+      },
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "country.reform_dict",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    },
+    {
+      "check_id": "population_uc_standard_allowance_plus_10",
+      "detail": {
+        "awards_lower": 0,
+        "payments_below_the_payable_floor": 0,
+        "payments_higher": 102,
+        "payments_lower": "fewer than 10",
+        "payments_not_award_less_deductions": 0,
+        "standard_allowance_scaled_10_percent": true,
+        "units_with_non_finite_values": 0
+      },
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "country.scenario_parameter_changes",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    },
+    {
+      "check_id": "population_uc_standard_allowance_plus_10",
+      "detail": {
+        "awards_lower": 0,
+        "payments_below_the_payable_floor": 0,
+        "payments_higher": 102,
+        "payments_lower": "fewer than 10",
+        "payments_not_award_less_deductions": 0,
+        "standard_allowance_scaled_10_percent": true,
+        "units_with_non_finite_values": 0
+      },
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "country.reform_from_dict",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    },
+    {
+      "check_id": "population_state_pension_plus_10",
+      "detail": {
+        "basic_and_new_scaled_10_percent": true,
+        "pensions_higher": 507,
+        "pensions_lower": 0
+      },
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "country.reform_dict",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    },
+    {
+      "check_id": "population_state_pension_plus_10",
+      "detail": {
+        "basic_and_new_scaled_10_percent": true,
+        "pensions_higher": 507,
+        "pensions_lower": 0
+      },
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "country.scenario_parameter_changes",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    },
+    {
+      "check_id": "population_state_pension_plus_10",
+      "detail": {
+        "basic_and_new_scaled_10_percent": true,
+        "pensions_higher": 507,
+        "pensions_lower": 0
+      },
+      "reform_identity": null,
+      "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+      "route": "country.reform_from_dict",
+      "seed": 0,
+      "status": "pass",
+      "subsample_households": 1000
+    }
+  ],
+  "prior_law_option": "projected",
+  "properties": [
+    {
+      "engine": "hypothesis",
+      "examples_run": 20,
+      "property_id": "annual_values_match_the_oracle",
+      "reform_identity": null,
+      "reform_identity_unavailable": "a sampled property runs many sampled reforms, so its row binds none",
+      "status": "pass"
+    },
+    {
+      "engine": "hypothesis",
+      "examples_run": 20,
+      "property_id": "routes_agree_and_baselines_hold",
+      "reform_identity": null,
+      "reform_identity_unavailable": "a sampled property runs many sampled reforms, so its row binds none",
+      "status": "pass"
+    }
+  ],
+  "routes": [
+    "country.reform_dict",
+    "country.scenario_parameter_changes",
+    "country.reform_from_dict",
+    "wrapper.household",
+    "wrapper.policy",
+    "api.sim_api"
+  ],
+  "seed": 0,
+  "summary": {
+    "error": 0,
+    "fail": 0,
+    "pass": 792,
+    "pending": 25,
+    "unsupported": 101
+  },
+  "versions": {
+    "policyengine": "6.4.0",
+    "policyengine-core": "3.33.0",
+    "policyengine-uk": "2.130.1"
+  }
+}
diff --git a/tests/fixtures/uk_contract/pinned_uk_2.125.1_defects.json b/tests/fixtures/uk_contract/pinned_uk_2.125.1_defects.json
new file mode 100644
index 00000000..e7d92fa8
--- /dev/null
+++ b/tests/fixtures/uk_contract/pinned_uk_2.125.1_defects.json
@@ -0,0 +1,2050 @@
+{
+ "cases": [
+  {
+   "actual": 0.2,
+   "case_id": "april_rate_change",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+   "decision_id": null,
+   "expected": 0.2,
+   "holds": null,
+   "oracle": "unchanged before 2027-28",
+   "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 0.2,
+   "case_id": "april_rate_change",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+   "decision_id": null,
+   "expected": 0.21,
+   "holds": null,
+   "oracle": "in force on 30 April 2027",
+   "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 0.21,
+   "case_id": "april_rate_change",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+   "decision_id": null,
+   "expected": 0.21,
+   "holds": null,
+   "oracle": "held from 6 April 2027",
+   "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 0.0,
+   "case_id": "april_rate_change",
+   "check": "change:income_tax:2026",
+   "decision_id": null,
+   "expected": 0.0,
+   "holds": null,
+   "oracle": "before the change",
+   "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 0.0,
+   "case_id": "april_rate_change",
+   "check": "change:income_tax:2027",
+   "decision_id": null,
+   "expected": 174.29999999999967,
+   "holds": null,
+   "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+   "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 174.300048828125,
+   "case_id": "april_rate_change",
+   "check": "change:income_tax:2028",
+   "decision_id": null,
+   "expected": 174.29999999999967,
+   "holds": null,
+   "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+   "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 3486.0,
+   "case_id": "april_rate_change",
+   "check": "baseline_isolation:income_tax:2026",
+   "expected": 3486.0,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 3486.0,
+   "case_id": "april_rate_change",
+   "check": "baseline_isolation:income_tax:2027",
+   "expected": 3486.0,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 3486.0,
+   "case_id": "april_rate_change",
+   "check": "baseline_isolation:income_tax:2028",
+   "expected": 3486.0,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "1cf30cfa43f9bcee4bbb7c3d7d42d481aafe57cb674b93709b952a6089e23665",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "case_id": "april_rate_change",
+   "check": null,
+   "detail": "ValueError: Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got: '2027-04-06.2100-12-31'.\nLearn more about legal period formats in OpenFisca:\n.",
+   "reform_digest": "c24ddebf09ef159ad38ebd530d2deca18d2ec7165400a6d92ada5c9d6175377e",
+   "route": "country.scenario_parameter_changes",
+   "status": "error"
+  },
+  {
+   "actual": 0.2,
+   "case_id": "april_rate_change",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+   "decision_id": null,
+   "expected": 0.2,
+   "holds": null,
+   "oracle": "unchanged before 2027-28",
+   "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 0.2,
+   "case_id": "april_rate_change",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+   "decision_id": null,
+   "expected": 0.21,
+   "holds": null,
+   "oracle": "in force on 30 April 2027",
+   "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 0.21,
+   "case_id": "april_rate_change",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+   "decision_id": null,
+   "expected": 0.21,
+   "holds": null,
+   "oracle": "held from 6 April 2027",
+   "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 0.0,
+   "case_id": "april_rate_change",
+   "check": "change:income_tax:2026",
+   "decision_id": null,
+   "expected": 0.0,
+   "holds": null,
+   "oracle": "before the change",
+   "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 0.0,
+   "case_id": "april_rate_change",
+   "check": "change:income_tax:2027",
+   "decision_id": null,
+   "expected": 174.29999999999967,
+   "holds": null,
+   "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+   "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 174.300048828125,
+   "case_id": "april_rate_change",
+   "check": "change:income_tax:2028",
+   "decision_id": null,
+   "expected": 174.29999999999967,
+   "holds": null,
+   "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+   "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 3486.0,
+   "case_id": "april_rate_change",
+   "check": "baseline_isolation:income_tax:2026",
+   "expected": 3486.0,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 3486.0,
+   "case_id": "april_rate_change",
+   "check": "baseline_isolation:income_tax:2027",
+   "expected": 3486.0,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 3486.0,
+   "case_id": "april_rate_change",
+   "check": "baseline_isolation:income_tax:2028",
+   "expected": 3486.0,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "eab1e87614e840127bd7b77723df34e4412b2cc05d1dee4326e255c08aae4087",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "case_id": "april_rate_change",
+   "check": null,
+   "detail": "TypeError: unhashable type: 'dict'",
+   "reform_digest": "f504a06f3382203f96a08fc1c07b86a0f001d4b40c0a2e805f6c0c4d29c50221",
+   "route": "wrapper.household",
+   "status": "error"
+  },
+  {
+   "case_id": "april_rate_change",
+   "check": null,
+   "detail": "KeyError: 'region'",
+   "reform_digest": "cd54b3b3b30cfd71448cbeb374c5d31aedca3d52733ab5c81e542feaba80704c",
+   "route": "wrapper.policy",
+   "status": "error"
+  },
+  {
+   "case_id": "april_rate_change",
+   "check": null,
+   "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+   "reform_digest": null,
+   "route": "api.sim_api",
+   "status": "unsupported"
+  },
+  {
+   "actual": 0.2,
+   "case_id": "finite_rate_change",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+   "decision_id": null,
+   "expected": 0.21,
+   "holds": null,
+   "oracle": "in force on 30 April 2027",
+   "reform_digest": "5a0ca24b82794f7c32123733efa38f90aa602676efa5219e4a13d97ff46f69ea",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 0.21,
+   "case_id": "finite_rate_change",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+   "decision_id": null,
+   "expected": 0.2,
+   "holds": null,
+   "oracle": "prior law after 5 April 2028",
+   "reform_digest": "5a0ca24b82794f7c32123733efa38f90aa602676efa5219e4a13d97ff46f69ea",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 0.0,
+   "case_id": "finite_rate_change",
+   "check": "change:income_tax:2027",
+   "decision_id": null,
+   "expected": 174.29999999999967,
+   "holds": null,
+   "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+   "reform_digest": "5a0ca24b82794f7c32123733efa38f90aa602676efa5219e4a13d97ff46f69ea",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 174.300048828125,
+   "case_id": "finite_rate_change",
+   "check": "change:income_tax:2028",
+   "decision_id": null,
+   "expected": 0.0,
+   "holds": null,
+   "oracle": "the change has ended",
+   "reform_digest": "5a0ca24b82794f7c32123733efa38f90aa602676efa5219e4a13d97ff46f69ea",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 3486.0,
+   "case_id": "finite_rate_change",
+   "check": "baseline_isolation:income_tax:2027",
+   "expected": 3486.0,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "5a0ca24b82794f7c32123733efa38f90aa602676efa5219e4a13d97ff46f69ea",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 3486.0,
+   "case_id": "finite_rate_change",
+   "check": "baseline_isolation:income_tax:2028",
+   "expected": 3486.0,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "5a0ca24b82794f7c32123733efa38f90aa602676efa5219e4a13d97ff46f69ea",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "case_id": "finite_rate_change",
+   "check": null,
+   "detail": "ValueError: Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got: '2027-04-06.2028-04-05'.\nLearn more about legal period formats in OpenFisca:\n.",
+   "reform_digest": "5e2b0da064082c7c363dec99c7e5bab2f190ae2f5ff9633454f1e10223293323",
+   "route": "country.scenario_parameter_changes",
+   "status": "error"
+  },
+  {
+   "actual": 0.2,
+   "case_id": "finite_rate_change",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+   "decision_id": null,
+   "expected": 0.21,
+   "holds": null,
+   "oracle": "in force on 30 April 2027",
+   "reform_digest": "ed4782036ee619f8c07483cac573d44bf1917fa33d3fa49a8551eebb632852ef",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 0.21,
+   "case_id": "finite_rate_change",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+   "decision_id": null,
+   "expected": 0.2,
+   "holds": null,
+   "oracle": "prior law after 5 April 2028",
+   "reform_digest": "ed4782036ee619f8c07483cac573d44bf1917fa33d3fa49a8551eebb632852ef",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 0.0,
+   "case_id": "finite_rate_change",
+   "check": "change:income_tax:2027",
+   "decision_id": null,
+   "expected": 174.29999999999967,
+   "holds": null,
+   "oracle": "(30,000 - personal allowance) x (new rate - current rate); the allowance and current rate are read from current law",
+   "reform_digest": "ed4782036ee619f8c07483cac573d44bf1917fa33d3fa49a8551eebb632852ef",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 174.300048828125,
+   "case_id": "finite_rate_change",
+   "check": "change:income_tax:2028",
+   "decision_id": null,
+   "expected": 0.0,
+   "holds": null,
+   "oracle": "the change has ended",
+   "reform_digest": "ed4782036ee619f8c07483cac573d44bf1917fa33d3fa49a8551eebb632852ef",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 3486.0,
+   "case_id": "finite_rate_change",
+   "check": "baseline_isolation:income_tax:2027",
+   "expected": 3486.0,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "ed4782036ee619f8c07483cac573d44bf1917fa33d3fa49a8551eebb632852ef",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 3486.0,
+   "case_id": "finite_rate_change",
+   "check": "baseline_isolation:income_tax:2028",
+   "expected": 3486.0,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "ed4782036ee619f8c07483cac573d44bf1917fa33d3fa49a8551eebb632852ef",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "case_id": "finite_rate_change",
+   "check": null,
+   "detail": "TypeError: unhashable type: 'dict'",
+   "reform_digest": "97674d142cf5719ae87a2523e758805be566fe02241aeff2a658fee27235c3f8",
+   "route": "wrapper.household",
+   "status": "error"
+  },
+  {
+   "case_id": "finite_rate_change",
+   "check": null,
+   "detail": "ValueError: unconverted data remains: .2028-04-05",
+   "reform_digest": "635e34257a38637cabd07234afb9e37355921ce7b680a98d8f85a1e05ecbbb64",
+   "route": "wrapper.policy",
+   "status": "error"
+  },
+  {
+   "case_id": "finite_rate_change",
+   "check": null,
+   "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+   "reform_digest": null,
+   "route": "api.sim_api",
+   "status": "unsupported"
+  },
+  {
+   "actual": 12821.408457811869,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031",
+   "decision_id": null,
+   "expected": 12570.0,
+   "holds": null,
+   "oracle": "frozen",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 38454.01293702494,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031",
+   "decision_id": null,
+   "expected": 37700.0,
+   "holds": null,
+   "oracle": "frozen",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 12821.408457811869,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2031",
+   "decision_id": null,
+   "expected": 12570.0,
+   "holds": null,
+   "oracle": "aligned with the allowance",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 51275.43382451891,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2031",
+   "decision_id": null,
+   "expected": 50270.0,
+   "holds": null,
+   "oracle": "allowance + basic rate limit",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 246.56476264971067,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031",
+   "decision_id": null,
+   "expected": 241.73,
+   "holds": null,
+   "oracle": "12,570 / 52, to the penny",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 986.0652504709999,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2031",
+   "decision_id": null,
+   "expected": 966.73,
+   "holds": null,
+   "oracle": "50,270 / 52, to the penny",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 0.0,
+   "case_id": "extended_freeze",
+   "check": "change:income_tax:2031",
+   "decision_id": null,
+   "expected": 50.28169156237382,
+   "holds": null,
+   "oracle": "basic rate x (current law's allowance - 12,570)",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 12570.0,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2032",
+   "decision_id": null,
+   "expected": 12570.0,
+   "holds": null,
+   "oracle": "frozen",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 37700.0,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2032",
+   "decision_id": null,
+   "expected": 37700.0,
+   "holds": null,
+   "oracle": "frozen",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 13077.821120979681,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2032",
+   "decision_id": null,
+   "expected": 12570.0,
+   "holds": null,
+   "oracle": "aligned with the allowance",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 52300.8804893913,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2032",
+   "decision_id": null,
+   "expected": 50270.0,
+   "holds": null,
+   "oracle": "allowance + basic rate limit",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 251.49575971156867,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2032",
+   "decision_id": null,
+   "expected": 241.73,
+   "holds": null,
+   "oracle": "12,570 / 52, to the penny",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 1005.7853629502536,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2032",
+   "decision_id": null,
+   "expected": 966.73,
+   "holds": null,
+   "oracle": "50,270 / 52, to the penny",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 101.599853515625,
+   "case_id": "extended_freeze",
+   "check": "change:income_tax:2032",
+   "decision_id": null,
+   "expected": 101.56422419593619,
+   "holds": null,
+   "oracle": "basic rate x (current law's allowance - 12,570)",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 12570.0,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2033",
+   "decision_id": null,
+   "expected": 12570.0,
+   "holds": null,
+   "oracle": "frozen",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 37700.0,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2033",
+   "decision_id": null,
+   "expected": 37700.0,
+   "holds": null,
+   "oracle": "frozen",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 13339.378953034586,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2033",
+   "decision_id": null,
+   "expected": 12570.0,
+   "holds": null,
+   "oracle": "aligned with the allowance",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 53346.903736598935,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2033",
+   "decision_id": null,
+   "expected": 50270.0,
+   "holds": null,
+   "oracle": "allowance + basic rate limit",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 256.52570201408514,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2033",
+   "decision_id": null,
+   "expected": 241.73,
+   "holds": null,
+   "oracle": "12,570 / 52, to the penny",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 1025.901178621092,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2033",
+   "decision_id": null,
+   "expected": 966.73,
+   "holds": null,
+   "oracle": "50,270 / 52, to the penny",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 154.0,
+   "case_id": "extended_freeze",
+   "check": "change:income_tax:2033",
+   "decision_id": null,
+   "expected": 153.87579060691715,
+   "holds": null,
+   "oracle": "basic rate x (current law's allowance - 12,570)",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 12570.0,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034",
+   "decision_id": null,
+   "expected": 13606.152435742164,
+   "holds": null,
+   "oracle": "contract 1.0.0: prior law's projected path resumes",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034",
+   "detail": "ValueError('Could not find the parameter gov.economic_assumptions.yoy_growth.september_cpi_uprating (failed at september_cpi_uprating).')",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "error"
+  },
+  {
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2034",
+   "detail": "ValueError('Could not find the parameter gov.economic_assumptions.yoy_growth.september_cpi_uprating (failed at september_cpi_uprating).')",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "error"
+  },
+  {
+   "actual": 3426.0,
+   "case_id": "extended_freeze",
+   "check": "baseline_isolation:income_tax:2031",
+   "expected": 3426.0,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 3374.800048828125,
+   "case_id": "extended_freeze",
+   "check": "baseline_isolation:income_tax:2032",
+   "expected": 3374.800048828125,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 3322.39990234375,
+   "case_id": "extended_freeze",
+   "check": "baseline_isolation:income_tax:2033",
+   "expected": 3322.39990234375,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "234ec5c3931182f8f76930655330ebbdb77a6e62586a0af81f386149be5c5036",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "case_id": "extended_freeze",
+   "check": null,
+   "detail": "ValueError: Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got: '2031-04-06.2034-04-05'.\nLearn more about legal period formats in OpenFisca:\n.",
+   "reform_digest": "725339aa214a4609707b6c10f7e32fdfb2491ad2e48da70f39a414dfc1f83aca",
+   "route": "country.scenario_parameter_changes",
+   "status": "error"
+  },
+  {
+   "actual": 12821.408457811869,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031",
+   "decision_id": null,
+   "expected": 12570.0,
+   "holds": null,
+   "oracle": "frozen",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 38454.01293702494,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031",
+   "decision_id": null,
+   "expected": 37700.0,
+   "holds": null,
+   "oracle": "frozen",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 12821.408457811869,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2031",
+   "decision_id": null,
+   "expected": 12570.0,
+   "holds": null,
+   "oracle": "aligned with the allowance",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 51275.43382451891,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2031",
+   "decision_id": null,
+   "expected": 50270.0,
+   "holds": null,
+   "oracle": "allowance + basic rate limit",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 246.56476264971067,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031",
+   "decision_id": null,
+   "expected": 241.73,
+   "holds": null,
+   "oracle": "12,570 / 52, to the penny",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 986.0652504709999,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2031",
+   "decision_id": null,
+   "expected": 966.73,
+   "holds": null,
+   "oracle": "50,270 / 52, to the penny",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 0.0,
+   "case_id": "extended_freeze",
+   "check": "change:income_tax:2031",
+   "decision_id": null,
+   "expected": 50.28169156237382,
+   "holds": null,
+   "oracle": "basic rate x (current law's allowance - 12,570)",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 12570.0,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2032",
+   "decision_id": null,
+   "expected": 12570.0,
+   "holds": null,
+   "oracle": "frozen",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 37700.0,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2032",
+   "decision_id": null,
+   "expected": 37700.0,
+   "holds": null,
+   "oracle": "frozen",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 13077.821120979681,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2032",
+   "decision_id": null,
+   "expected": 12570.0,
+   "holds": null,
+   "oracle": "aligned with the allowance",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 52300.8804893913,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2032",
+   "decision_id": null,
+   "expected": 50270.0,
+   "holds": null,
+   "oracle": "allowance + basic rate limit",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 251.49575971156867,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2032",
+   "decision_id": null,
+   "expected": 241.73,
+   "holds": null,
+   "oracle": "12,570 / 52, to the penny",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 1005.7853629502536,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2032",
+   "decision_id": null,
+   "expected": 966.73,
+   "holds": null,
+   "oracle": "50,270 / 52, to the penny",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 101.599853515625,
+   "case_id": "extended_freeze",
+   "check": "change:income_tax:2032",
+   "decision_id": null,
+   "expected": 101.56422419593619,
+   "holds": null,
+   "oracle": "basic rate x (current law's allowance - 12,570)",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 12570.0,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2033",
+   "decision_id": null,
+   "expected": 12570.0,
+   "holds": null,
+   "oracle": "frozen",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 37700.0,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2033",
+   "decision_id": null,
+   "expected": 37700.0,
+   "holds": null,
+   "oracle": "frozen",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 13339.378953034586,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.lower_profits_limit:2033",
+   "decision_id": null,
+   "expected": 12570.0,
+   "holds": null,
+   "oracle": "aligned with the allowance",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 53346.903736598935,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_4.thresholds.upper_profits_limit:2033",
+   "decision_id": null,
+   "expected": 50270.0,
+   "holds": null,
+   "oracle": "allowance + basic rate limit",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 256.52570201408514,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2033",
+   "decision_id": null,
+   "expected": 241.73,
+   "holds": null,
+   "oracle": "12,570 / 52, to the penny",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 1025.901178621092,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.upper_earnings_limit:2033",
+   "decision_id": null,
+   "expected": 966.73,
+   "holds": null,
+   "oracle": "50,270 / 52, to the penny",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 154.0,
+   "case_id": "extended_freeze",
+   "check": "change:income_tax:2033",
+   "decision_id": null,
+   "expected": 153.87579060691715,
+   "holds": null,
+   "oracle": "basic rate x (current law's allowance - 12,570)",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 12570.0,
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034",
+   "decision_id": null,
+   "expected": 13606.152435742164,
+   "holds": null,
+   "oracle": "contract 1.0.0: prior law's projected path resumes",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034",
+   "detail": "ValueError('Could not find the parameter gov.economic_assumptions.yoy_growth.september_cpi_uprating (failed at september_cpi_uprating).')",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "error"
+  },
+  {
+   "case_id": "extended_freeze",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2034",
+   "detail": "ValueError('Could not find the parameter gov.economic_assumptions.yoy_growth.september_cpi_uprating (failed at september_cpi_uprating).')",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "error"
+  },
+  {
+   "actual": 3426.0,
+   "case_id": "extended_freeze",
+   "check": "baseline_isolation:income_tax:2031",
+   "expected": 3426.0,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 3374.800048828125,
+   "case_id": "extended_freeze",
+   "check": "baseline_isolation:income_tax:2032",
+   "expected": 3374.800048828125,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 3322.39990234375,
+   "case_id": "extended_freeze",
+   "check": "baseline_isolation:income_tax:2033",
+   "expected": 3322.39990234375,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "423fc235458423a35a13511dffa5b18190c7e1d0e7b3a209d4b923708dd647f0",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "case_id": "extended_freeze",
+   "check": null,
+   "detail": "TypeError: unhashable type: 'dict'",
+   "reform_digest": "b53bec711f02356d55dbb011544c0e556eba7559496d89d10ae266da9e217a0e",
+   "route": "wrapper.household",
+   "status": "error"
+  },
+  {
+   "case_id": "extended_freeze",
+   "check": null,
+   "detail": "ValueError: unconverted data remains: .2034-04-05",
+   "reform_digest": "f0cebbfddd5f9a74d7ebb7f83247004201148486b8714cd916087872ce2dfaaa",
+   "route": "wrapper.policy",
+   "status": "error"
+  },
+  {
+   "case_id": "extended_freeze",
+   "check": null,
+   "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+   "reform_digest": null,
+   "route": "api.sim_api",
+   "status": "unsupported"
+  },
+  {
+   "actual": 5216.06103515625,
+   "case_id": "april_benefit_uprating",
+   "check": "variable:uc_standard_allowance:2027",
+   "decision_id": null,
+   "expected": 6000.0,
+   "holds": null,
+   "oracle": "\u00a3500 x 12",
+   "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 6000.0,
+   "case_id": "april_benefit_uprating",
+   "check": "variable:uc_standard_allowance:2028",
+   "decision_id": null,
+   "expected": 6240.0,
+   "holds": null,
+   "oracle": "\u00a3520 x 12",
+   "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 6240.0,
+   "case_id": "april_benefit_uprating",
+   "check": "variable:uc_standard_allowance:2029",
+   "decision_id": null,
+   "expected": 6240.0,
+   "holds": null,
+   "oracle": "held from April 2028",
+   "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 0.0,
+   "case_id": "april_benefit_uprating",
+   "check": "change:uc_standard_allowance:2026",
+   "decision_id": null,
+   "expected": 0.0,
+   "holds": null,
+   "oracle": "before the change",
+   "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 5216.06103515625,
+   "case_id": "april_benefit_uprating",
+   "check": "baseline_isolation:uc_standard_allowance:2027",
+   "expected": 5216.06103515625,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 5320.3740234375,
+   "case_id": "april_benefit_uprating",
+   "check": "baseline_isolation:uc_standard_allowance:2028",
+   "expected": 5320.3740234375,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 5426.7763671875,
+   "case_id": "april_benefit_uprating",
+   "check": "baseline_isolation:uc_standard_allowance:2029",
+   "expected": 5426.7763671875,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 5098.7998046875,
+   "case_id": "april_benefit_uprating",
+   "check": "baseline_isolation:uc_standard_allowance:2026",
+   "expected": 5098.7998046875,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "e98d41db2ed7d855dda05d65e66e1e4ef4b4167088a2d88bbd92c9b3260402ff",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "case_id": "april_benefit_uprating",
+   "check": null,
+   "detail": "ValueError: Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got: '2027-04-06.2028-04-05'.\nLearn more about legal period formats in OpenFisca:\n.",
+   "reform_digest": "0ab5df46a43311bce2af6c4b9b70155c64df30388c8a0f99f15a2846351f715a",
+   "route": "country.scenario_parameter_changes",
+   "status": "error"
+  },
+  {
+   "actual": 5216.06103515625,
+   "case_id": "april_benefit_uprating",
+   "check": "variable:uc_standard_allowance:2027",
+   "decision_id": null,
+   "expected": 6000.0,
+   "holds": null,
+   "oracle": "\u00a3500 x 12",
+   "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 6000.0,
+   "case_id": "april_benefit_uprating",
+   "check": "variable:uc_standard_allowance:2028",
+   "decision_id": null,
+   "expected": 6240.0,
+   "holds": null,
+   "oracle": "\u00a3520 x 12",
+   "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 6240.0,
+   "case_id": "april_benefit_uprating",
+   "check": "variable:uc_standard_allowance:2029",
+   "decision_id": null,
+   "expected": 6240.0,
+   "holds": null,
+   "oracle": "held from April 2028",
+   "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 0.0,
+   "case_id": "april_benefit_uprating",
+   "check": "change:uc_standard_allowance:2026",
+   "decision_id": null,
+   "expected": 0.0,
+   "holds": null,
+   "oracle": "before the change",
+   "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 5216.06103515625,
+   "case_id": "april_benefit_uprating",
+   "check": "baseline_isolation:uc_standard_allowance:2027",
+   "expected": 5216.06103515625,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 5320.3740234375,
+   "case_id": "april_benefit_uprating",
+   "check": "baseline_isolation:uc_standard_allowance:2028",
+   "expected": 5320.3740234375,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 5426.7763671875,
+   "case_id": "april_benefit_uprating",
+   "check": "baseline_isolation:uc_standard_allowance:2029",
+   "expected": 5426.7763671875,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 5098.7998046875,
+   "case_id": "april_benefit_uprating",
+   "check": "baseline_isolation:uc_standard_allowance:2026",
+   "expected": 5098.7998046875,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "33b86ab43be584c8c6e8aafdd55053dcf72078a3a525cd638f38bcd1d645227f",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "case_id": "april_benefit_uprating",
+   "check": null,
+   "detail": "TypeError: unhashable type: 'dict'",
+   "reform_digest": "dfc339899202506f1bb7300cea49e605d3c128f1c036c0ec5df0c3d326d0c33e",
+   "route": "wrapper.household",
+   "status": "error"
+  },
+  {
+   "case_id": "april_benefit_uprating",
+   "check": null,
+   "detail": "ValueError: unconverted data remains: .2028-04-05",
+   "reform_digest": "d4388eb374c51a6da454f1325fe7699aca1a9004b95dae95464c3a9b2aea8bd5",
+   "route": "wrapper.policy",
+   "status": "error"
+  },
+  {
+   "case_id": "april_benefit_uprating",
+   "check": null,
+   "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+   "reform_digest": null,
+   "route": "api.sim_api",
+   "status": "unsupported"
+  },
+  {
+   "actual": 5216.06103515625,
+   "case_id": "bounded_change_on_uprated_benefit",
+   "check": "variable:uc_standard_allowance:2027",
+   "decision_id": null,
+   "expected": 6000.0,
+   "holds": null,
+   "oracle": "\u00a3500 x 12",
+   "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 679.6259765625,
+   "case_id": "bounded_change_on_uprated_benefit",
+   "check": "change:uc_standard_allowance:2028",
+   "decision_id": null,
+   "expected": 0.0,
+   "holds": null,
+   "oracle": "prior law's uprated path resumes",
+   "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 0.0,
+   "case_id": "bounded_change_on_uprated_benefit",
+   "check": "change:uc_standard_allowance:2030",
+   "decision_id": null,
+   "expected": 0.0,
+   "holds": null,
+   "oracle": "prior law's uprated path resumes",
+   "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 5216.06103515625,
+   "case_id": "bounded_change_on_uprated_benefit",
+   "check": "baseline_isolation:uc_standard_allowance:2027",
+   "expected": 5216.06103515625,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 5320.3740234375,
+   "case_id": "bounded_change_on_uprated_benefit",
+   "check": "baseline_isolation:uc_standard_allowance:2028",
+   "expected": 5320.3740234375,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 5535.2998046875,
+   "case_id": "bounded_change_on_uprated_benefit",
+   "check": "baseline_isolation:uc_standard_allowance:2030",
+   "expected": 5535.2998046875,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "d646393894abdc73e4d317eff0c39419cfbf5f69df4dfea57f44db7700f1fd54",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "case_id": "bounded_change_on_uprated_benefit",
+   "check": null,
+   "detail": "ValueError: Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got: '2027-04-06.2028-04-05'.\nLearn more about legal period formats in OpenFisca:\n.",
+   "reform_digest": "ba3722100cf3de0524dfd14b8e0b498a4c2caebe66bdf38395ae8d4568589236",
+   "route": "country.scenario_parameter_changes",
+   "status": "error"
+  },
+  {
+   "actual": 5216.06103515625,
+   "case_id": "bounded_change_on_uprated_benefit",
+   "check": "variable:uc_standard_allowance:2027",
+   "decision_id": null,
+   "expected": 6000.0,
+   "holds": null,
+   "oracle": "\u00a3500 x 12",
+   "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 679.6259765625,
+   "case_id": "bounded_change_on_uprated_benefit",
+   "check": "change:uc_standard_allowance:2028",
+   "decision_id": null,
+   "expected": 0.0,
+   "holds": null,
+   "oracle": "prior law's uprated path resumes",
+   "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 0.0,
+   "case_id": "bounded_change_on_uprated_benefit",
+   "check": "change:uc_standard_allowance:2030",
+   "decision_id": null,
+   "expected": 0.0,
+   "holds": null,
+   "oracle": "prior law's uprated path resumes",
+   "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 5216.06103515625,
+   "case_id": "bounded_change_on_uprated_benefit",
+   "check": "baseline_isolation:uc_standard_allowance:2027",
+   "expected": 5216.06103515625,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 5320.3740234375,
+   "case_id": "bounded_change_on_uprated_benefit",
+   "check": "baseline_isolation:uc_standard_allowance:2028",
+   "expected": 5320.3740234375,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 5535.2998046875,
+   "case_id": "bounded_change_on_uprated_benefit",
+   "check": "baseline_isolation:uc_standard_allowance:2030",
+   "expected": 5535.2998046875,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "20b97441158d9b07c0a62a5bfd332aaa4ab9749ac9a9d3b93f046461f54acfc4",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "case_id": "bounded_change_on_uprated_benefit",
+   "check": null,
+   "detail": "TypeError: unhashable type: 'dict'",
+   "reform_digest": "6df62a9f41db002ef95cadf25f2cd566de071a424141ebd1c479e4398ead4a4e",
+   "route": "wrapper.household",
+   "status": "error"
+  },
+  {
+   "case_id": "bounded_change_on_uprated_benefit",
+   "check": null,
+   "detail": "ValueError: unconverted data remains: .2028-04-05",
+   "reform_digest": "48f71ddba48e9494e868325cf1af191218d7edb5c4846616dbe75500e6db417d",
+   "route": "wrapper.policy",
+   "status": "error"
+  },
+  {
+   "case_id": "bounded_change_on_uprated_benefit",
+   "check": null,
+   "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+   "reform_digest": null,
+   "route": "api.sim_api",
+   "status": "unsupported"
+  },
+  {
+   "actual": 0.18,
+   "case_id": "mid_year_blended_rate",
+   "check": "parameter:gov.hmrc.cgt.basic_rate:2027",
+   "decision_id": null,
+   "expected": 0.18868852459016502,
+   "holds": null,
+   "oracle": "day-weighted: 207 days at 18% and 159 at 20% of 366",
+   "reform_digest": "3d8fd4c48241e68e1e1d8479b2b2067b274092a7c4fc2476b7cea829c18aa588",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 0.2,
+   "case_id": "mid_year_blended_rate",
+   "check": "parameter:gov.hmrc.cgt.basic_rate:2028",
+   "decision_id": null,
+   "expected": 0.2,
+   "holds": null,
+   "oracle": "in force all year",
+   "reform_digest": "3d8fd4c48241e68e1e1d8479b2b2067b274092a7c4fc2476b7cea829c18aa588",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 0.18000000000000002,
+   "case_id": "mid_year_blended_rate",
+   "check": "parameter:gov.hmrc.cgt.basic_rate:2026",
+   "decision_id": null,
+   "expected": 0.18,
+   "holds": null,
+   "oracle": "before the change",
+   "reform_digest": "3d8fd4c48241e68e1e1d8479b2b2067b274092a7c4fc2476b7cea829c18aa588",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "case_id": "mid_year_blended_rate",
+   "check": null,
+   "detail": "ValueError: Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got: '2027-10-30.2100-12-31'.\nLearn more about legal period formats in OpenFisca:\n.",
+   "reform_digest": "576ce816dacf70876bdfd765e57f48d44a9499181cca2b3e178dfaf51cbad0bd",
+   "route": "country.scenario_parameter_changes",
+   "status": "error"
+  },
+  {
+   "actual": 0.18,
+   "case_id": "mid_year_blended_rate",
+   "check": "parameter:gov.hmrc.cgt.basic_rate:2027",
+   "decision_id": null,
+   "expected": 0.18868852459016502,
+   "holds": null,
+   "oracle": "day-weighted: 207 days at 18% and 159 at 20% of 366",
+   "reform_digest": "abe5e3d10b5669e954ed794facadad012121eef44cc1444801edb99a6c902081",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 0.2,
+   "case_id": "mid_year_blended_rate",
+   "check": "parameter:gov.hmrc.cgt.basic_rate:2028",
+   "decision_id": null,
+   "expected": 0.2,
+   "holds": null,
+   "oracle": "in force all year",
+   "reform_digest": "abe5e3d10b5669e954ed794facadad012121eef44cc1444801edb99a6c902081",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 0.18000000000000002,
+   "case_id": "mid_year_blended_rate",
+   "check": "parameter:gov.hmrc.cgt.basic_rate:2026",
+   "decision_id": null,
+   "expected": 0.18,
+   "holds": null,
+   "oracle": "before the change",
+   "reform_digest": "abe5e3d10b5669e954ed794facadad012121eef44cc1444801edb99a6c902081",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "case_id": "mid_year_blended_rate",
+   "check": "parameter:gov.hmrc.cgt.basic_rate:2027",
+   "reason": "this route exposes outputs, not parameter values",
+   "reform_digest": "f9f1430783586c75c5ce96f54a3e4ee30af28059391bcfb9d0cb16d4230184e5",
+   "route": "wrapper.household",
+   "status": "unsupported"
+  },
+  {
+   "case_id": "mid_year_blended_rate",
+   "check": "parameter:gov.hmrc.cgt.basic_rate:2028",
+   "reason": "this route exposes outputs, not parameter values",
+   "reform_digest": "f9f1430783586c75c5ce96f54a3e4ee30af28059391bcfb9d0cb16d4230184e5",
+   "route": "wrapper.household",
+   "status": "unsupported"
+  },
+  {
+   "case_id": "mid_year_blended_rate",
+   "check": "parameter:gov.hmrc.cgt.basic_rate:2026",
+   "reason": "this route exposes outputs, not parameter values",
+   "reform_digest": "f9f1430783586c75c5ce96f54a3e4ee30af28059391bcfb9d0cb16d4230184e5",
+   "route": "wrapper.household",
+   "status": "unsupported"
+  },
+  {
+   "case_id": "mid_year_blended_rate",
+   "check": "parameter:gov.hmrc.cgt.basic_rate:2027",
+   "reason": "this route exposes outputs, not parameter values",
+   "reform_digest": "772a73501537a9c16f95dadad9ad48d044520ef6f92b0f53345c881b8250b5b7",
+   "route": "wrapper.policy",
+   "status": "unsupported"
+  },
+  {
+   "case_id": "mid_year_blended_rate",
+   "check": "parameter:gov.hmrc.cgt.basic_rate:2028",
+   "reason": "this route exposes outputs, not parameter values",
+   "reform_digest": "772a73501537a9c16f95dadad9ad48d044520ef6f92b0f53345c881b8250b5b7",
+   "route": "wrapper.policy",
+   "status": "unsupported"
+  },
+  {
+   "case_id": "mid_year_blended_rate",
+   "check": "parameter:gov.hmrc.cgt.basic_rate:2026",
+   "reason": "this route exposes outputs, not parameter values",
+   "reform_digest": "772a73501537a9c16f95dadad9ad48d044520ef6f92b0f53345c881b8250b5b7",
+   "route": "wrapper.policy",
+   "status": "unsupported"
+  },
+  {
+   "case_id": "mid_year_blended_rate",
+   "check": null,
+   "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+   "reform_digest": null,
+   "route": "api.sim_api",
+   "status": "unsupported"
+  },
+  {
+   "actual": 12821.408457811869,
+   "case_id": "september_cpi_moves_indexed_thresholds",
+   "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031",
+   "decision_id": null,
+   "expected": 13200.0,
+   "holds": null,
+   "oracle": "ITA 2007 s57(3)",
+   "reform_digest": "8bd9476925c59e9d13946308e4604e33976dc8a1d68b63e276628c95135a82d0",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 38454.01293702494,
+   "case_id": "september_cpi_moves_indexed_thresholds",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031",
+   "decision_id": null,
+   "expected": 39600.0,
+   "holds": null,
+   "oracle": "ITA 2007 s21(3)",
+   "reform_digest": "8bd9476925c59e9d13946308e4604e33976dc8a1d68b63e276628c95135a82d0",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 246.56476264971067,
+   "case_id": "september_cpi_moves_indexed_thresholds",
+   "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031",
+   "decision_id": null,
+   "expected": 253.85,
+   "holds": null,
+   "oracle": "aligned with the allowance, weekly",
+   "reform_digest": "8bd9476925c59e9d13946308e4604e33976dc8a1d68b63e276628c95135a82d0",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "case_id": "september_cpi_moves_indexed_thresholds",
+   "check": null,
+   "detail": "ValueError: Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got: '2030-09-01.2031-08-31'.\nLearn more about legal period formats in OpenFisca:\n.",
+   "reform_digest": "afe5d61179ae6fd02453405015018bcbdd8047fcc3e69e23c43e5f57dce17c67",
+   "route": "country.scenario_parameter_changes",
+   "status": "error"
+  },
+  {
+   "actual": 12821.408457811869,
+   "case_id": "september_cpi_moves_indexed_thresholds",
+   "check": "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2031",
+   "decision_id": null,
+   "expected": 13200.0,
+   "holds": null,
+   "oracle": "ITA 2007 s57(3)",
+   "reform_digest": "fda4228463d1ee4cd7263094d2b28ffb49c78d09b87468542dba7e6821ff3f9d",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 38454.01293702494,
+   "case_id": "september_cpi_moves_indexed_thresholds",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[1].threshold:2031",
+   "decision_id": null,
+   "expected": 39600.0,
+   "holds": null,
+   "oracle": "ITA 2007 s21(3)",
+   "reform_digest": "fda4228463d1ee4cd7263094d2b28ffb49c78d09b87468542dba7e6821ff3f9d",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 246.56476264971067,
+   "case_id": "september_cpi_moves_indexed_thresholds",
+   "check": "parameter:gov.hmrc.national_insurance.class_1.thresholds.primary_threshold:2031",
+   "decision_id": null,
+   "expected": 253.85,
+   "holds": null,
+   "oracle": "aligned with the allowance, weekly",
+   "reform_digest": "fda4228463d1ee4cd7263094d2b28ffb49c78d09b87468542dba7e6821ff3f9d",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "case_id": "uc_rebalancing_reform_after_data_load",
+   "check": null,
+   "detail": "ValueError: Could not find the parameter gov.dwp.universal_credit.rebalancing.protected_health_element (failed at protected_health_element).",
+   "reform_digest": "12bcb296368c2b63f30b2040bfa5c84678f0bd52aaf7f85d510a75cd70f995c3",
+   "route": "country.reform_dict",
+   "status": "error"
+  },
+  {
+   "case_id": "uc_rebalancing_reform_after_data_load",
+   "check": null,
+   "detail": "ValueError: Could not find the parameter gov.dwp.universal_credit.rebalancing.protected_health_element (failed at protected_health_element).",
+   "reform_digest": "909e6875b8d449e87b5d3eb0ad74e584e80241b9f40677343430b41232b20c5f",
+   "route": "country.scenario_parameter_changes",
+   "status": "error"
+  },
+  {
+   "case_id": "uc_rebalancing_reform_after_data_load",
+   "check": null,
+   "detail": "ValueError: Could not find the parameter gov.dwp.universal_credit.rebalancing.protected_health_element (failed at protected_health_element).",
+   "reform_digest": "9947c105e6b609c8a5f2c13ce4707daad40cfa8529bf2487734ef2a513169e8d",
+   "route": "country.reform_from_dict",
+   "status": "error"
+  },
+  {
+   "case_id": "uc_rebalancing_reform_after_data_load",
+   "check": null,
+   "detail": "ValueError: Reform contains parameter paths not defined on policyengine-uk 2.125.1:\n  - 'gov.dwp.universal_credit.rebalancing.protected_health_element' (did you mean 'gov.dwp.universal_credit.rebalancing.new_claimant_health_element'?)",
+   "reform_digest": "a695bb7865a505dbbcb9c973b674a5666804e15838252c66bb7e84fa082388f0",
+   "route": "wrapper.household",
+   "status": "error"
+  },
+  {
+   "case_id": "uc_rebalancing_reform_after_data_load",
+   "check": null,
+   "detail": "ValueError: Reform contains parameter paths not defined on policyengine-uk 2.125.1:\n  - 'gov.dwp.universal_credit.rebalancing.protected_health_element' (did you mean 'gov.dwp.universal_credit.rebalancing.new_claimant_health_element'?)",
+   "reform_digest": "88900943775d8451bee562422107eca7016ee21042a516b295fe3c00bc9a7da9",
+   "route": "wrapper.policy",
+   "status": "error"
+  },
+  {
+   "case_id": "uc_rebalancing_reform_after_data_load",
+   "check": null,
+   "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+   "reform_digest": null,
+   "route": "api.sim_api",
+   "status": "unsupported"
+  },
+  {
+   "actual": 0.0,
+   "case_id": "structural_switch",
+   "check": "variable:marriage_allowance:2026",
+   "decision_id": null,
+   "expected": 1260.0,
+   "holds": null,
+   "oracle": "10% of the \u00a312,570 allowance, rounded up to \u00a310 (ITA 2007 s55B)",
+   "reform_digest": "d837da2e6fabde655a05b130322e0c29d06d5d493ad99183a699198dc5e1d4b2",
+   "route": "country.reform_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 0.0,
+   "case_id": "structural_switch",
+   "check": "baseline_isolation:marriage_allowance:2026",
+   "expected": 0.0,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "d837da2e6fabde655a05b130322e0c29d06d5d493ad99183a699198dc5e1d4b2",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "case_id": "structural_switch",
+   "check": null,
+   "detail": "ValueError: Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got: '2025-01-01.2030-12-31'.\nLearn more about legal period formats in OpenFisca:\n.",
+   "reform_digest": "f0cad18ed901a4062c12dd721fa663915f3329fad55f54bbe407922de196d60d",
+   "route": "country.scenario_parameter_changes",
+   "status": "error"
+  },
+  {
+   "actual": 0.0,
+   "case_id": "structural_switch",
+   "check": "variable:marriage_allowance:2026",
+   "decision_id": null,
+   "expected": 1260.0,
+   "holds": null,
+   "oracle": "10% of the \u00a312,570 allowance, rounded up to \u00a310 (ITA 2007 s55B)",
+   "reform_digest": "19b28590adbd8d9b80069a5504380f0226f0a1e5b1b2f73f5ba411fc562d7742",
+   "route": "country.reform_from_dict",
+   "status": "fail"
+  },
+  {
+   "actual": 0.0,
+   "case_id": "structural_switch",
+   "check": "baseline_isolation:marriage_allowance:2026",
+   "expected": 0.0,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "19b28590adbd8d9b80069a5504380f0226f0a1e5b1b2f73f5ba411fc562d7742",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "case_id": "structural_switch",
+   "check": null,
+   "detail": "TypeError: unhashable type: 'dict'",
+   "reform_digest": "f76cc80028ce85017f3730a4d7db21c8a69f7b68bae0123c0aed45df087b1393",
+   "route": "wrapper.household",
+   "status": "error"
+  },
+  {
+   "case_id": "structural_switch",
+   "check": null,
+   "detail": "ValueError: unconverted data remains: .2030-12-31",
+   "reform_digest": "6e7e98694b32def0a541b52e0abb79b1cdcfc1e50a70784147d24a17e652ffe0",
+   "route": "wrapper.policy",
+   "status": "error"
+  },
+  {
+   "case_id": "structural_switch",
+   "check": null,
+   "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+   "reform_digest": null,
+   "route": "api.sim_api",
+   "status": "unsupported"
+  },
+  {
+   "actual": 5263.158203125,
+   "case_id": "baseline_vat_is_current_law",
+   "check": "baseline_equals:baseline_vat:2026",
+   "decision_id": null,
+   "expected": 5263.158203125,
+   "holds": null,
+   "oracle": "baseline_vat is the unreformed vat",
+   "reform_digest": "062a65e94c93f043b9ff9322b07152c930d1e1cb375237eb2375343a9e470e73",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 5263.158203125,
+   "case_id": "baseline_vat_is_current_law",
+   "check": "baseline_isolation:baseline_vat:2026",
+   "expected": 5263.158203125,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "062a65e94c93f043b9ff9322b07152c930d1e1cb375237eb2375343a9e470e73",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 5263.158203125,
+   "case_id": "baseline_vat_is_current_law",
+   "check": "baseline_isolation:vat:2026",
+   "expected": 5263.158203125,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "062a65e94c93f043b9ff9322b07152c930d1e1cb375237eb2375343a9e470e73",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "case_id": "baseline_vat_is_current_law",
+   "check": null,
+   "detail": "ValueError: Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got: '2026-01-01.2100-12-31'.\nLearn more about legal period formats in OpenFisca:\n.",
+   "reform_digest": "8a1e1a66ee167c2dc554452d46828576b58159ed3d3276ffa1723f417d5ade0d",
+   "route": "country.scenario_parameter_changes",
+   "status": "error"
+  },
+  {
+   "actual": 5263.158203125,
+   "case_id": "baseline_vat_is_current_law",
+   "check": "baseline_equals:baseline_vat:2026",
+   "decision_id": null,
+   "expected": 5263.158203125,
+   "holds": null,
+   "oracle": "baseline_vat is the unreformed vat",
+   "reform_digest": "8df662d00e257aeda318ef34432e8de4ab0474fcd093ab9afd16e5fd97c4f096",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 5263.158203125,
+   "case_id": "baseline_vat_is_current_law",
+   "check": "baseline_isolation:baseline_vat:2026",
+   "expected": 5263.158203125,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "8df662d00e257aeda318ef34432e8de4ab0474fcd093ab9afd16e5fd97c4f096",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 5263.158203125,
+   "case_id": "baseline_vat_is_current_law",
+   "check": "baseline_isolation:vat:2026",
+   "expected": 5263.158203125,
+   "oracle": "the reformed simulation's baseline equals an unreformed simulation",
+   "reform_digest": "8df662d00e257aeda318ef34432e8de4ab0474fcd093ab9afd16e5fd97c4f096",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "case_id": "baseline_vat_is_current_law",
+   "check": null,
+   "detail": "TypeError: unhashable type: 'dict'",
+   "reform_digest": "ee8e8cffbe252201a18cc7581c191e14f1b0269bfcfaf9d48cde191e9b2aaae9",
+   "route": "wrapper.household",
+   "status": "error"
+  },
+  {
+   "actual": "ValueError: Could not find the parameter gov.budget_2026.new_credit.amount (failed at budget_2026).",
+   "case_id": "unknown_path",
+   "check": "rejected",
+   "expected": "an error matching 'new_credit'",
+   "reform_digest": "8465ddfc32335f10bb9d23b050f0156a37c48e8a10dc35719d6b7e4f6d1a9238",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": "ValueError: Could not find the parameter gov.budget_2026.new_credit.amount (failed at budget_2026).",
+   "case_id": "unknown_path",
+   "check": "rejected",
+   "expected": "an error matching 'new_credit'",
+   "reform_digest": "a72b9604c243f77845e1446379e85bd2eca8e0fa7c671956a26e05ecadc3c4a7",
+   "route": "country.scenario_parameter_changes",
+   "status": "pass"
+  },
+  {
+   "actual": "ValueError: Could not find the parameter gov.budget_2026.new_credit.amount (failed at budget_2026).",
+   "case_id": "unknown_path",
+   "check": "rejected",
+   "expected": "an error matching 'new_credit'",
+   "reform_digest": "32c326f8ca94bac9e220000e7f0fcf1a34bccd4947357e3819d419bb6ff193ea",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": "ValueError: Reform contains parameter paths not defined on policyengine-uk 2.125.1:\n  - 'gov.budget_2026.new_credit.amount'",
+   "case_id": "unknown_path",
+   "check": "rejected",
+   "expected": "an error matching 'new_credit'",
+   "reform_digest": "292d055c4ba7f1a326b9fa798441a0f6d2186c07206ce7d63177d829371ee335",
+   "route": "wrapper.household",
+   "status": "pass"
+  },
+  {
+   "actual": "ValueError: Reform contains parameter paths not defined on policyengine-uk 2.125.1:\n  - 'gov.budget_2026.new_credit.amount'",
+   "case_id": "unknown_path",
+   "check": "rejected",
+   "expected": "an error matching 'new_credit'",
+   "reform_digest": "29a0b52eda9615814b814d0de2932d01d8d59430150dcbf9fbb2fcdf79d1fd8d",
+   "route": "wrapper.policy",
+   "status": "pass"
+  },
+  {
+   "case_id": "unknown_path",
+   "check": null,
+   "reason": "Runs in policyengine-sim-api's deploy gate, which imports CASES and routes them through its runtime; it is not callable in-process.",
+   "reform_digest": null,
+   "route": "api.sim_api",
+   "status": "unsupported"
+  },
+  {
+   "actual": 0.21,
+   "case_id": "reading_bare_year_dict",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+   "decision_id": null,
+   "expected": 0.21,
+   "holds": null,
+   "oracle": "fiscal year 2027",
+   "reform_digest": "3314f6d0362de9643ce2022904e4fcbbe84297c25d36c37add61a878c7dc3e9c",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 0.2,
+   "case_id": "reading_bare_year_dict",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+   "decision_id": null,
+   "expected": 0.2,
+   "holds": null,
+   "oracle": "that year only",
+   "reform_digest": "3314f6d0362de9643ce2022904e4fcbbe84297c25d36c37add61a878c7dc3e9c",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 0.2,
+   "case_id": "reading_bare_year_reform_from_dict",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2026",
+   "decision_id": null,
+   "expected": 0.2,
+   "holds": null,
+   "oracle": "before 1 January 2027",
+   "reform_digest": "d19ea548203c2417e712543d47546f3fa48c741252ccd5b6bf1664d9a517d6b6",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 0.21,
+   "case_id": "reading_bare_year_reform_from_dict",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+   "decision_id": null,
+   "expected": 0.21,
+   "holds": null,
+   "oracle": "from 1 January 2027",
+   "reform_digest": "d19ea548203c2417e712543d47546f3fa48c741252ccd5b6bf1664d9a517d6b6",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 0.21,
+   "case_id": "reading_bare_year_reform_from_dict",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+   "decision_id": null,
+   "expected": 0.21,
+   "holds": null,
+   "oracle": "onwards",
+   "reform_digest": "d19ea548203c2417e712543d47546f3fa48c741252ccd5b6bf1664d9a517d6b6",
+   "route": "country.reform_from_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 0.2,
+   "case_id": "reading_single_day_dict",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2027",
+   "decision_id": null,
+   "expected": 0.2,
+   "holds": null,
+   "oracle": "one day is not 30 April",
+   "reform_digest": "dec76eb1ea64e800caff37168afaaa26599bb03b533c169908d691c111cdbc36",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "actual": 0.2,
+   "case_id": "reading_single_day_dict",
+   "check": "parameter:gov.hmrc.income_tax.rates.uk[0].rate:2028",
+   "decision_id": null,
+   "expected": 0.2,
+   "holds": null,
+   "oracle": "one day only",
+   "reform_digest": "dec76eb1ea64e800caff37168afaaa26599bb03b533c169908d691c111cdbc36",
+   "route": "country.reform_dict",
+   "status": "pass"
+  },
+  {
+   "case_id": "reading_effective_dates_reverse_order",
+   "check": null,
+   "detail": "TypeError: unhashable type: 'dict'",
+   "reform_digest": "56c7ad7d1ddf6185703a5f60d31f11a12272e1166c667996b484801f57dbebcd",
+   "route": "wrapper.household",
+   "status": "error"
+  },
+  {
+   "case_id": "reading_effective_dates_reverse_order",
+   "check": null,
+   "detail": "KeyError: 'region'",
+   "reform_digest": "b09577ce022e4cc30b4702add142a1e3aa1ab438a27955d182bba894199cecd5",
+   "route": "wrapper.policy",
+   "status": "error"
+  }
+ ],
+ "contract_id": "policyengine-uk/budget-reform-contract",
+ "contract_version": "1.0.0",
+ "dataset": null,
+ "elapsed_seconds": 280.6,
+ "holds": false,
+ "max_examples": 2,
+ "population": [],
+ "prior_law_option": "projected",
+ "properties": [
+  {
+   "detail": "ValueError(\"Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got: '2025-06-01.2100-12-31'.\\nLearn more about legal period formats in OpenFisca:\\n.\")",
+   "engine": "hypothesis",
+   "examples_run": 2,
+   "property_id": "annual_values_match_the_oracle",
+   "status": "error"
+  },
+  {
+   "detail": "ValueError(\"Expected a period (eg. '2017', '2017-01', '2017-01-01', ...); got: '2025-06-01.2100-12-31'.\\nLearn more about legal period formats in OpenFisca:\\n.\")",
+   "engine": "hypothesis",
+   "examples_run": 2,
+   "property_id": "routes_agree_and_baselines_hold",
+   "status": "error"
+  }
+ ],
+ "routes": [
+  "country.reform_dict",
+  "country.scenario_parameter_changes",
+  "country.reform_from_dict",
+  "wrapper.household",
+  "wrapper.policy",
+  "api.sim_api"
+ ],
+ "seed": 0,
+ "summary": {
+  "error": 35,
+  "fail": 66,
+  "pass": 76,
+  "pending": 0,
+  "unsupported": 15
+ },
+ "versions": {
+  "policyengine": "6.2.6",
+  "policyengine-core": "3.33.0",
+  "policyengine-uk": "2.125.1"
+ }
+}
diff --git a/tests/fixtures/uk_contract/population_pass_max_examples_0.json b/tests/fixtures/uk_contract/population_pass_max_examples_0.json
new file mode 100644
index 00000000..22db1db2
--- /dev/null
+++ b/tests/fixtures/uk_contract/population_pass_max_examples_0.json
@@ -0,0 +1,161 @@
+{
+ "cases": [],
+ "contract_id": "policyengine-uk/budget-reform-contract",
+ "contract_version": "1.0.0",
+ "dataset": {
+  "path": "",
+  "sha256": "5ba399ea5ab9a6c7d4e9aaf6c623080beb34d9ad5f330f6244dbad72b8d960b6"
+ },
+ "elapsed_seconds": 23.6,
+ "holds": true,
+ "max_examples": 0,
+ "population": [
+  {
+   "check_id": "population_baseline_isolation",
+   "detail": null,
+   "route": "country.reform_dict",
+   "seed": 0,
+   "status": "pass",
+   "subsample_households": 1000
+  },
+  {
+   "check_id": "population_baseline_isolation",
+   "detail": null,
+   "route": "country.scenario_parameter_changes",
+   "seed": 0,
+   "status": "pass",
+   "subsample_households": 1000
+  },
+  {
+   "check_id": "population_baseline_isolation",
+   "detail": null,
+   "route": "country.reform_from_dict",
+   "seed": 0,
+   "status": "pass",
+   "subsample_households": 1000
+  },
+  {
+   "check_id": "population_baseline_isolation",
+   "detail": null,
+   "route": "unreformed",
+   "seed": 0,
+   "status": "pass",
+   "subsample_households": 1000
+  },
+  {
+   "check_id": "population_query_order",
+   "detail": null,
+   "route": "unreformed",
+   "seed": 0,
+   "status": "pass",
+   "subsample_households": 1000
+  },
+  {
+   "check_id": "population_routes_agree",
+   "route": "country.reform_dict = country.scenario_parameter_changes",
+   "seed": 0,
+   "status": "pass",
+   "subsample_households": 1000
+  },
+  {
+   "check_id": "population_routes_agree",
+   "route": "country.reform_dict = country.reform_from_dict",
+   "seed": 0,
+   "status": "pass",
+   "subsample_households": 1000
+  },
+  {
+   "check_id": "population_april_rate_change",
+   "detail": {
+    "2026_unchanged": true,
+    "2027_people_with_higher_tax": 1093,
+    "2027_people_with_lower_tax": 0
+   },
+   "route": "country.reform_dict",
+   "seed": 0,
+   "status": "pass",
+   "subsample_households": 1000
+  },
+  {
+   "check_id": "population_april_rate_change",
+   "detail": {
+    "2026_unchanged": true,
+    "2027_people_with_higher_tax": 1093,
+    "2027_people_with_lower_tax": 0
+   },
+   "route": "country.scenario_parameter_changes",
+   "seed": 0,
+   "status": "pass",
+   "subsample_households": 1000
+  },
+  {
+   "check_id": "population_april_rate_change",
+   "detail": {
+    "2026_unchanged": true,
+    "2027_people_with_higher_tax": 1093,
+    "2027_people_with_lower_tax": 0
+   },
+   "route": "country.reform_from_dict",
+   "seed": 0,
+   "status": "pass",
+   "subsample_households": 1000
+  },
+  {
+   "check_id": "population_finite_change_expires",
+   "route": "country.reform_dict",
+   "seed": 0,
+   "status": "pass",
+   "subsample_households": 1000
+  },
+  {
+   "check_id": "population_finite_change_expires",
+   "route": "country.scenario_parameter_changes",
+   "seed": 0,
+   "status": "pass",
+   "subsample_households": 1000
+  },
+  {
+   "check_id": "population_finite_change_expires",
+   "route": "country.reform_from_dict",
+   "seed": 0,
+   "status": "pass",
+   "subsample_households": 1000
+  }
+ ],
+ "prior_law_option": "projected",
+ "properties": [
+  {
+   "examples_run": 0,
+   "property_id": "annual_values_match_the_oracle",
+   "reason": "max_examples is 0, so no examples were requested",
+   "status": "unsupported"
+  },
+  {
+   "examples_run": 0,
+   "property_id": "routes_agree_and_baselines_hold",
+   "reason": "max_examples is 0, so no examples were requested",
+   "status": "unsupported"
+  }
+ ],
+ "routes": [
+  "country.reform_dict",
+  "country.scenario_parameter_changes",
+  "country.reform_from_dict",
+  "wrapper.household",
+  "wrapper.policy",
+  "api.sim_api"
+ ],
+ "seed": 0,
+ "summary": {
+  "error": 0,
+  "fail": 0,
+  "pass": 13,
+  "pending": 0,
+  "unsupported": 2
+ },
+ "versions": {
+  "policyengine": "6.2.6",
+  "policyengine-core": "3.33.0",
+  "policyengine-uk": "2.126.8"
+ }
+}
diff --git a/tests/fixtures/uk_metrics/README.md b/tests/fixtures/uk_metrics/README.md
new file mode 100644
index 00000000..166265e7
--- /dev/null
+++ b/tests/fixtures/uk_metrics/README.md
@@ -0,0 +1,31 @@
+# UK metrics tool fixtures
+
+Both files come from PolicyEngine/policyengine-uk's aggregate-impact tool
+(`policyengine_uk/utils/aggregate_impact/`, policyengine-uk#2335).
+`tests/test_uk_candidate_gates.py` reads them. Refresh them when a pin changes
+the report; never edit them by hand.
+
+- `budget_metrics_report.json`: a real `compute_budget_metrics` report from
+  the tool's owner (workstream W3), at tool commit
+  `1191ac5a473419f48e0c0643f83d31719da788e1`, on the certified pair: uk-data
+  1.58.1 (`enhanced_frs_2024_25`) with policyengine-uk 2.130.1 and
+  policyengine-core 3.33.0, for 2025-26 to 2031-32, with the overlap guard and
+  the ten standard reforms. It holds aggregates only, under the tool's
+  small-cell rule (`privacy.py`): a figure from fewer than 10 records is
+  published as `value` null with `suppressed` true, and every total that
+  holds one is rebuilt without it and lists it under `excludes`, with the
+  sign it enters with under `exclude_signs`. It has 1,484 metric rows (14
+  suppressed: Disabled Students' Allowance's total and caseload in each of
+  the seven years; 112 rows in 16 totals rebuilt without it) and 350 checks.
+- `standard_reforms.yaml`: the tool's standard reform set at the same commit,
+  copied unchanged. Ten reforms, each with the model variable it must move,
+  the direction and the least it must move it in every year (a fifth of its
+  smallest yearly effect on the certified pair, adopted under cos d1344).
+  The reform responsiveness gate reads the same file from the vendored tool.
+
+Its values, suppression and checks are byte-identical to W3's report at
+tool commit 228a05a3e, which first replaced the old fixture; 1191ac5a4 adds
+only `exclude_signs`. That replaced a 1.58.0 report from before the tool
+suppressed small cells. That report published Disabled Students' Allowance, which comes from
+fewer than 10 records, unsuppressed. Never commit a report from a tool
+commit that predates suppression.
diff --git a/tests/fixtures/uk_metrics/budget_metrics_report.json b/tests/fixtures/uk_metrics/budget_metrics_report.json
new file mode 100644
index 00000000..6e79f9c5
--- /dev/null
+++ b/tests/fixtures/uk_metrics/budget_metrics_report.json
@@ -0,0 +1,21199 @@
+{
+ "schema": "policyengine-uk-aggregate-impact/1/report",
+ "status": "ok",
+ "metrics_version": "policyengine-uk-aggregate-impact/1+2e07ec2dcecd5de3+bb5198a0d8e7c046",
+ "partition_version": "e8ea8a0198736e7cd9b0725148fdda717911033ee768eba044b58a8d46e9b0ef",
+ "years": [
+  2025,
+  2026,
+  2027,
+  2028,
+  2029,
+  2030,
+  2031
+ ],
+ "metrics": [
+  {
+   "metric_id": "population.person",
+   "year": 2025,
+   "value": 69513955.15348345,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "People"
+  },
+  {
+   "metric_id": "population.person",
+   "year": 2026,
+   "value": 69778110.5666471,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "People"
+  },
+  {
+   "metric_id": "population.person",
+   "year": 2027,
+   "value": 70036290.77594277,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "People"
+  },
+  {
+   "metric_id": "population.person",
+   "year": 2028,
+   "value": 70316432.3587328,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "People"
+  },
+  {
+   "metric_id": "population.person",
+   "year": 2029,
+   "value": 70625825.64596106,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "People"
+  },
+  {
+   "metric_id": "population.person",
+   "year": 2030,
+   "value": 70943644.05542476,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "People"
+  },
+  {
+   "metric_id": "population.person",
+   "year": 2031,
+   "value": 71248701.50975493,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "People"
+  },
+  {
+   "metric_id": "population.benunit",
+   "year": 2025,
+   "value": 35246926.333675,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "Benefit units"
+  },
+  {
+   "metric_id": "population.benunit",
+   "year": 2026,
+   "value": 35380865.871866405,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "Benefit units"
+  },
+  {
+   "metric_id": "population.benunit",
+   "year": 2027,
+   "value": 35511775.69777489,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "Benefit units"
+  },
+  {
+   "metric_id": "population.benunit",
+   "year": 2028,
+   "value": 35653820.972568765,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "Benefit units"
+  },
+  {
+   "metric_id": "population.benunit",
+   "year": 2029,
+   "value": 35810698.281318754,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "Benefit units"
+  },
+  {
+   "metric_id": "population.benunit",
+   "year": 2030,
+   "value": 35971847.531772316,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "Benefit units"
+  },
+  {
+   "metric_id": "population.benunit",
+   "year": 2031,
+   "value": 36126526.36007868,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "Benefit units"
+  },
+  {
+   "metric_id": "population.household",
+   "year": 2025,
+   "value": 31166694.04532416,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "Households"
+  },
+  {
+   "metric_id": "population.household",
+   "year": 2026,
+   "value": 31285128.558958113,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "Households"
+  },
+  {
+   "metric_id": "population.household",
+   "year": 2027,
+   "value": 31400884.081961572,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "Households"
+  },
+  {
+   "metric_id": "population.household",
+   "year": 2028,
+   "value": 31526486.005968377,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "Households"
+  },
+  {
+   "metric_id": "population.household",
+   "year": 2029,
+   "value": 31665202.984048083,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "Households"
+  },
+  {
+   "metric_id": "population.household",
+   "year": 2030,
+   "value": 31807697.37639664,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "Households"
+  },
+  {
+   "metric_id": "population.household",
+   "year": 2031,
+   "value": 31944470.379216716,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "population",
+   "partition": null,
+   "label": "Households"
+  },
+  {
+   "metric_id": "receipts.income_tax",
+   "year": 2025,
+   "value": 297984459607.39465,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "income_tax",
+   "label": "Income tax"
+  },
+  {
+   "metric_id": "receipts.income_tax",
+   "year": 2026,
+   "value": 317782510223.38354,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "income_tax",
+   "label": "Income tax"
+  },
+  {
+   "metric_id": "receipts.income_tax",
+   "year": 2027,
+   "value": 335140035823.943,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "income_tax",
+   "label": "Income tax"
+  },
+  {
+   "metric_id": "receipts.income_tax",
+   "year": 2028,
+   "value": 350031424626.733,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "income_tax",
+   "label": "Income tax"
+  },
+  {
+   "metric_id": "receipts.income_tax",
+   "year": 2029,
+   "value": 366458443148.21484,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "income_tax",
+   "label": "Income tax"
+  },
+  {
+   "metric_id": "receipts.income_tax",
+   "year": 2030,
+   "value": 384662297800.91766,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "income_tax",
+   "label": "Income tax"
+  },
+  {
+   "metric_id": "receipts.income_tax",
+   "year": 2031,
+   "value": 403275325039.3891,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "income_tax",
+   "label": "Income tax"
+  },
+  {
+   "metric_id": "receipts.nics_individual",
+   "year": 2025,
+   "value": 52575017190.03174,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "nics_individual",
+   "label": "National Insurance (employee and self-employed)"
+  },
+  {
+   "metric_id": "receipts.nics_individual",
+   "year": 2026,
+   "value": 54941380589.82846,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "nics_individual",
+   "label": "National Insurance (employee and self-employed)"
+  },
+  {
+   "metric_id": "receipts.nics_individual",
+   "year": 2027,
+   "value": 56752394914.42203,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "nics_individual",
+   "label": "National Insurance (employee and self-employed)"
+  },
+  {
+   "metric_id": "receipts.nics_individual",
+   "year": 2028,
+   "value": 58476221815.846825,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "nics_individual",
+   "label": "National Insurance (employee and self-employed)"
+  },
+  {
+   "metric_id": "receipts.nics_individual",
+   "year": 2029,
+   "value": 60511486452.76118,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "nics_individual",
+   "label": "National Insurance (employee and self-employed)"
+  },
+  {
+   "metric_id": "receipts.nics_individual",
+   "year": 2030,
+   "value": 62445204124.271996,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "nics_individual",
+   "label": "National Insurance (employee and self-employed)"
+  },
+  {
+   "metric_id": "receipts.nics_individual",
+   "year": 2031,
+   "value": 64898705801.9577,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "nics_individual",
+   "label": "National Insurance (employee and self-employed)"
+  },
+  {
+   "metric_id": "receipts.nics_employer",
+   "year": 2025,
+   "value": 152640935215.8076,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "nics_employer",
+   "label": "National Insurance (employer)"
+  },
+  {
+   "metric_id": "receipts.nics_employer",
+   "year": 2026,
+   "value": 159182492481.57574,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "nics_employer",
+   "label": "National Insurance (employer)"
+  },
+  {
+   "metric_id": "receipts.nics_employer",
+   "year": 2027,
+   "value": 164141628899.95114,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "nics_employer",
+   "label": "National Insurance (employer)"
+  },
+  {
+   "metric_id": "receipts.nics_employer",
+   "year": 2028,
+   "value": 168731462725.2504,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "nics_employer",
+   "label": "National Insurance (employer)"
+  },
+  {
+   "metric_id": "receipts.nics_employer",
+   "year": 2029,
+   "value": 175566267043.68347,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "nics_employer",
+   "label": "National Insurance (employer)"
+  },
+  {
+   "metric_id": "receipts.nics_employer",
+   "year": 2030,
+   "value": 181079291564.9595,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "nics_employer",
+   "label": "National Insurance (employer)"
+  },
+  {
+   "metric_id": "receipts.nics_employer",
+   "year": 2031,
+   "value": 188223742975.49728,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "nics_employer",
+   "label": "National Insurance (employer)"
+  },
+  {
+   "metric_id": "receipts.employer_nics_response_capital",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "employer_nics_response_capital",
+   "label": "Employer NI change borne by capital"
+  },
+  {
+   "metric_id": "receipts.employer_nics_response_capital",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "employer_nics_response_capital",
+   "label": "Employer NI change borne by capital"
+  },
+  {
+   "metric_id": "receipts.employer_nics_response_capital",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "employer_nics_response_capital",
+   "label": "Employer NI change borne by capital"
+  },
+  {
+   "metric_id": "receipts.employer_nics_response_capital",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "employer_nics_response_capital",
+   "label": "Employer NI change borne by capital"
+  },
+  {
+   "metric_id": "receipts.employer_nics_response_capital",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "employer_nics_response_capital",
+   "label": "Employer NI change borne by capital"
+  },
+  {
+   "metric_id": "receipts.employer_nics_response_capital",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "employer_nics_response_capital",
+   "label": "Employer NI change borne by capital"
+  },
+  {
+   "metric_id": "receipts.employer_nics_response_capital",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "employer_nics_response_capital",
+   "label": "Employer NI change borne by capital"
+  },
+  {
+   "metric_id": "receipts.employer_nics_response_consumer",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "employer_nics_response_consumer",
+   "label": "Employer NI change borne by consumers"
+  },
+  {
+   "metric_id": "receipts.employer_nics_response_consumer",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "employer_nics_response_consumer",
+   "label": "Employer NI change borne by consumers"
+  },
+  {
+   "metric_id": "receipts.employer_nics_response_consumer",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "employer_nics_response_consumer",
+   "label": "Employer NI change borne by consumers"
+  },
+  {
+   "metric_id": "receipts.employer_nics_response_consumer",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "employer_nics_response_consumer",
+   "label": "Employer NI change borne by consumers"
+  },
+  {
+   "metric_id": "receipts.employer_nics_response_consumer",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "employer_nics_response_consumer",
+   "label": "Employer NI change borne by consumers"
+  },
+  {
+   "metric_id": "receipts.employer_nics_response_consumer",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "employer_nics_response_consumer",
+   "label": "Employer NI change borne by consumers"
+  },
+  {
+   "metric_id": "receipts.employer_nics_response_consumer",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "employer_nics_response_consumer",
+   "label": "Employer NI change borne by consumers"
+  },
+  {
+   "metric_id": "receipts.capital_gains_tax",
+   "year": 2025,
+   "value": 12618588087.194477,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "capital_gains_tax",
+   "label": "Capital gains tax"
+  },
+  {
+   "metric_id": "receipts.capital_gains_tax",
+   "year": 2026,
+   "value": 13055343054.272093,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "capital_gains_tax",
+   "label": "Capital gains tax"
+  },
+  {
+   "metric_id": "receipts.capital_gains_tax",
+   "year": 2027,
+   "value": 13547024733.440294,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "capital_gains_tax",
+   "label": "Capital gains tax"
+  },
+  {
+   "metric_id": "receipts.capital_gains_tax",
+   "year": 2028,
+   "value": 14041998268.40096,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "capital_gains_tax",
+   "label": "Capital gains tax"
+  },
+  {
+   "metric_id": "receipts.capital_gains_tax",
+   "year": 2029,
+   "value": 14540347118.814587,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "capital_gains_tax",
+   "label": "Capital gains tax"
+  },
+  {
+   "metric_id": "receipts.capital_gains_tax",
+   "year": 2030,
+   "value": 15086908668.221151,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "capital_gains_tax",
+   "label": "Capital gains tax"
+  },
+  {
+   "metric_id": "receipts.capital_gains_tax",
+   "year": 2031,
+   "value": 15645569134.728756,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "capital_gains_tax",
+   "label": "Capital gains tax"
+  },
+  {
+   "metric_id": "receipts.student_loan_repayments",
+   "year": 2025,
+   "value": 7826615852.272217,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "student_loan_repayments",
+   "label": "Student loan repayments"
+  },
+  {
+   "metric_id": "receipts.student_loan_repayments",
+   "year": 2026,
+   "value": 8123473394.454311,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "student_loan_repayments",
+   "label": "Student loan repayments"
+  },
+  {
+   "metric_id": "receipts.student_loan_repayments",
+   "year": 2027,
+   "value": 8349215176.222883,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "student_loan_repayments",
+   "label": "Student loan repayments"
+  },
+  {
+   "metric_id": "receipts.student_loan_repayments",
+   "year": 2028,
+   "value": 8558646443.275475,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "student_loan_repayments",
+   "label": "Student loan repayments"
+  },
+  {
+   "metric_id": "receipts.student_loan_repayments",
+   "year": 2029,
+   "value": 8785423316.325344,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "student_loan_repayments",
+   "label": "Student loan repayments"
+  },
+  {
+   "metric_id": "receipts.student_loan_repayments",
+   "year": 2030,
+   "value": 9036756985.870995,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "student_loan_repayments",
+   "label": "Student loan repayments"
+  },
+  {
+   "metric_id": "receipts.student_loan_repayments",
+   "year": 2031,
+   "value": 9376925443.173416,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "student_loan_repayments",
+   "label": "Student loan repayments"
+  },
+  {
+   "metric_id": "receipts.vat",
+   "year": 2025,
+   "value": 313127412006.5256,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vat",
+   "label": "VAT"
+  },
+  {
+   "metric_id": "receipts.vat",
+   "year": 2026,
+   "value": 321546605195.24274,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vat",
+   "label": "VAT"
+  },
+  {
+   "metric_id": "receipts.vat",
+   "year": 2027,
+   "value": 329191061310.5304,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vat",
+   "label": "VAT"
+  },
+  {
+   "metric_id": "receipts.vat",
+   "year": 2028,
+   "value": 337117963034.16547,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vat",
+   "label": "VAT"
+  },
+  {
+   "metric_id": "receipts.vat",
+   "year": 2029,
+   "value": 345373312536.52356,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vat",
+   "label": "VAT"
+  },
+  {
+   "metric_id": "receipts.vat",
+   "year": 2030,
+   "value": 353866055269.635,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vat",
+   "label": "VAT"
+  },
+  {
+   "metric_id": "receipts.vat",
+   "year": 2031,
+   "value": 362495430544.1016,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vat",
+   "label": "VAT"
+  },
+  {
+   "metric_id": "receipts.vat_change",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vat_change",
+   "label": "Change in VAT from its baseline"
+  },
+  {
+   "metric_id": "receipts.vat_change",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vat_change",
+   "label": "Change in VAT from its baseline"
+  },
+  {
+   "metric_id": "receipts.vat_change",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vat_change",
+   "label": "Change in VAT from its baseline"
+  },
+  {
+   "metric_id": "receipts.vat_change",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vat_change",
+   "label": "Change in VAT from its baseline"
+  },
+  {
+   "metric_id": "receipts.vat_change",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vat_change",
+   "label": "Change in VAT from its baseline"
+  },
+  {
+   "metric_id": "receipts.vat_change",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vat_change",
+   "label": "Change in VAT from its baseline"
+  },
+  {
+   "metric_id": "receipts.vat_change",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vat_change",
+   "label": "Change in VAT from its baseline"
+  },
+  {
+   "metric_id": "receipts.private_school_vat",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "private_school_vat",
+   "label": "VAT on private school fees"
+  },
+  {
+   "metric_id": "receipts.private_school_vat",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "private_school_vat",
+   "label": "VAT on private school fees"
+  },
+  {
+   "metric_id": "receipts.private_school_vat",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "private_school_vat",
+   "label": "VAT on private school fees"
+  },
+  {
+   "metric_id": "receipts.private_school_vat",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "private_school_vat",
+   "label": "VAT on private school fees"
+  },
+  {
+   "metric_id": "receipts.private_school_vat",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "private_school_vat",
+   "label": "VAT on private school fees"
+  },
+  {
+   "metric_id": "receipts.private_school_vat",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "private_school_vat",
+   "label": "VAT on private school fees"
+  },
+  {
+   "metric_id": "receipts.private_school_vat",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "private_school_vat",
+   "label": "VAT on private school fees"
+  },
+  {
+   "metric_id": "receipts.fuel_duty",
+   "year": 2025,
+   "value": 24534379561.8306,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "fuel_duty",
+   "label": "Fuel duties"
+  },
+  {
+   "metric_id": "receipts.fuel_duty",
+   "year": 2026,
+   "value": 24639271891.62144,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "fuel_duty",
+   "label": "Fuel duties"
+  },
+  {
+   "metric_id": "receipts.fuel_duty",
+   "year": 2027,
+   "value": 26638194760.36437,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "fuel_duty",
+   "label": "Fuel duties"
+  },
+  {
+   "metric_id": "receipts.fuel_duty",
+   "year": 2028,
+   "value": 26694679049.418755,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "fuel_duty",
+   "label": "Fuel duties"
+  },
+  {
+   "metric_id": "receipts.fuel_duty",
+   "year": 2029,
+   "value": 26418117577.300167,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "fuel_duty",
+   "label": "Fuel duties"
+  },
+  {
+   "metric_id": "receipts.fuel_duty",
+   "year": 2030,
+   "value": 25760349910.6802,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "fuel_duty",
+   "label": "Fuel duties"
+  },
+  {
+   "metric_id": "receipts.fuel_duty",
+   "year": 2031,
+   "value": 26353950047.77822,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "fuel_duty",
+   "label": "Fuel duties"
+  },
+  {
+   "metric_id": "receipts.alcohol_duty",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "alcohol_duty",
+   "label": "Alcohol duties"
+  },
+  {
+   "metric_id": "receipts.alcohol_duty",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "alcohol_duty",
+   "label": "Alcohol duties"
+  },
+  {
+   "metric_id": "receipts.alcohol_duty",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "alcohol_duty",
+   "label": "Alcohol duties"
+  },
+  {
+   "metric_id": "receipts.alcohol_duty",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "alcohol_duty",
+   "label": "Alcohol duties"
+  },
+  {
+   "metric_id": "receipts.alcohol_duty",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "alcohol_duty",
+   "label": "Alcohol duties"
+  },
+  {
+   "metric_id": "receipts.alcohol_duty",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "alcohol_duty",
+   "label": "Alcohol duties"
+  },
+  {
+   "metric_id": "receipts.alcohol_duty",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "alcohol_duty",
+   "label": "Alcohol duties"
+  },
+  {
+   "metric_id": "receipts.tobacco_duty",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "tobacco_duty",
+   "label": "Tobacco duties"
+  },
+  {
+   "metric_id": "receipts.tobacco_duty",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "tobacco_duty",
+   "label": "Tobacco duties"
+  },
+  {
+   "metric_id": "receipts.tobacco_duty",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "tobacco_duty",
+   "label": "Tobacco duties"
+  },
+  {
+   "metric_id": "receipts.tobacco_duty",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "tobacco_duty",
+   "label": "Tobacco duties"
+  },
+  {
+   "metric_id": "receipts.tobacco_duty",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "tobacco_duty",
+   "label": "Tobacco duties"
+  },
+  {
+   "metric_id": "receipts.tobacco_duty",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "tobacco_duty",
+   "label": "Tobacco duties"
+  },
+  {
+   "metric_id": "receipts.tobacco_duty",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "tobacco_duty",
+   "label": "Tobacco duties"
+  },
+  {
+   "metric_id": "receipts.vehicle_excise_duty",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vehicle_excise_duty",
+   "label": "Vehicle excise duties"
+  },
+  {
+   "metric_id": "receipts.vehicle_excise_duty",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vehicle_excise_duty",
+   "label": "Vehicle excise duties"
+  },
+  {
+   "metric_id": "receipts.vehicle_excise_duty",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vehicle_excise_duty",
+   "label": "Vehicle excise duties"
+  },
+  {
+   "metric_id": "receipts.vehicle_excise_duty",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vehicle_excise_duty",
+   "label": "Vehicle excise duties"
+  },
+  {
+   "metric_id": "receipts.vehicle_excise_duty",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vehicle_excise_duty",
+   "label": "Vehicle excise duties"
+  },
+  {
+   "metric_id": "receipts.vehicle_excise_duty",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vehicle_excise_duty",
+   "label": "Vehicle excise duties"
+  },
+  {
+   "metric_id": "receipts.vehicle_excise_duty",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "vehicle_excise_duty",
+   "label": "Vehicle excise duties"
+  },
+  {
+   "metric_id": "receipts.tv_licence",
+   "year": 2025,
+   "value": 4275969952.7872777,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "tv_licence",
+   "label": "TV licence fee"
+  },
+  {
+   "metric_id": "receipts.tv_licence",
+   "year": 2026,
+   "value": 4427595383.179631,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "tv_licence",
+   "label": "TV licence fee"
+  },
+  {
+   "metric_id": "receipts.tv_licence",
+   "year": 2027,
+   "value": 4444328769.554615,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "tv_licence",
+   "label": "TV licence fee"
+  },
+  {
+   "metric_id": "receipts.tv_licence",
+   "year": 2028,
+   "value": 4464697036.417519,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "tv_licence",
+   "label": "TV licence fee"
+  },
+  {
+   "metric_id": "receipts.tv_licence",
+   "year": 2029,
+   "value": 4485128769.03225,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "tv_licence",
+   "label": "TV licence fee"
+  },
+  {
+   "metric_id": "receipts.tv_licence",
+   "year": 2030,
+   "value": 4506096907.015281,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "tv_licence",
+   "label": "TV licence fee"
+  },
+  {
+   "metric_id": "receipts.tv_licence",
+   "year": 2031,
+   "value": 4526586455.751576,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "tv_licence",
+   "label": "TV licence fee"
+  },
+  {
+   "metric_id": "receipts.council_tax",
+   "year": 2025,
+   "value": 49560513009.82745,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "council_tax",
+   "label": "Council tax"
+  },
+  {
+   "metric_id": "receipts.council_tax",
+   "year": 2026,
+   "value": 52449635526.310104,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "council_tax",
+   "label": "Council tax"
+  },
+  {
+   "metric_id": "receipts.council_tax",
+   "year": 2027,
+   "value": 55778039679.1455,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "council_tax",
+   "label": "Council tax"
+  },
+  {
+   "metric_id": "receipts.council_tax",
+   "year": 2028,
+   "value": 59386561832.62195,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "council_tax",
+   "label": "Council tax"
+  },
+  {
+   "metric_id": "receipts.council_tax",
+   "year": 2029,
+   "value": 62981354541.126625,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "council_tax",
+   "label": "Council tax"
+  },
+  {
+   "metric_id": "receipts.council_tax",
+   "year": 2030,
+   "value": 66736811407.45229,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "council_tax",
+   "label": "Council tax"
+  },
+  {
+   "metric_id": "receipts.council_tax",
+   "year": 2031,
+   "value": 70704732540.45876,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "council_tax",
+   "label": "Council tax"
+  },
+  {
+   "metric_id": "receipts.high_value_council_tax_surcharge",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "high_value_council_tax_surcharge",
+   "label": "High value council tax surcharge"
+  },
+  {
+   "metric_id": "receipts.high_value_council_tax_surcharge",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "high_value_council_tax_surcharge",
+   "label": "High value council tax surcharge"
+  },
+  {
+   "metric_id": "receipts.high_value_council_tax_surcharge",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "high_value_council_tax_surcharge",
+   "label": "High value council tax surcharge"
+  },
+  {
+   "metric_id": "receipts.high_value_council_tax_surcharge",
+   "year": 2028,
+   "value": 483900104.6233177,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "high_value_council_tax_surcharge",
+   "label": "High value council tax surcharge"
+  },
+  {
+   "metric_id": "receipts.high_value_council_tax_surcharge",
+   "year": 2029,
+   "value": 495749379.82161856,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "high_value_council_tax_surcharge",
+   "label": "High value council tax surcharge"
+  },
+  {
+   "metric_id": "receipts.high_value_council_tax_surcharge",
+   "year": 2030,
+   "value": 507938784.7696847,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "high_value_council_tax_surcharge",
+   "label": "High value council tax surcharge"
+  },
+  {
+   "metric_id": "receipts.high_value_council_tax_surcharge",
+   "year": 2031,
+   "value": 520325557.6779943,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "high_value_council_tax_surcharge",
+   "label": "High value council tax surcharge"
+  },
+  {
+   "metric_id": "receipts.domestic_rates",
+   "year": 2025,
+   "value": 552492559.6753057,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "domestic_rates",
+   "label": "Northern Ireland domestic rates"
+  },
+  {
+   "metric_id": "receipts.domestic_rates",
+   "year": 2026,
+   "value": 578605888.7461181,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "domestic_rates",
+   "label": "Northern Ireland domestic rates"
+  },
+  {
+   "metric_id": "receipts.domestic_rates",
+   "year": 2027,
+   "value": 605893088.8719264,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "domestic_rates",
+   "label": "Northern Ireland domestic rates"
+  },
+  {
+   "metric_id": "receipts.domestic_rates",
+   "year": 2028,
+   "value": 634656724.1676605,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "domestic_rates",
+   "label": "Northern Ireland domestic rates"
+  },
+  {
+   "metric_id": "receipts.domestic_rates",
+   "year": 2029,
+   "value": 665050775.6261593,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "domestic_rates",
+   "label": "Northern Ireland domestic rates"
+  },
+  {
+   "metric_id": "receipts.domestic_rates",
+   "year": 2030,
+   "value": 696969823.2250919,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "domestic_rates",
+   "label": "Northern Ireland domestic rates"
+  },
+  {
+   "metric_id": "receipts.domestic_rates",
+   "year": 2031,
+   "value": 730275344.6123755,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "domestic_rates",
+   "label": "Northern Ireland domestic rates"
+  },
+  {
+   "metric_id": "receipts.business_rates",
+   "year": 2025,
+   "value": 31732501414.791157,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "business_rates",
+   "label": "Business rates"
+  },
+  {
+   "metric_id": "receipts.business_rates",
+   "year": 2026,
+   "value": 31732500125.540802,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "business_rates",
+   "label": "Business rates"
+  },
+  {
+   "metric_id": "receipts.business_rates",
+   "year": 2027,
+   "value": 31732498126.72371,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "business_rates",
+   "label": "Business rates"
+  },
+  {
+   "metric_id": "receipts.business_rates",
+   "year": 2028,
+   "value": 31732499316.105175,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "business_rates",
+   "label": "Business rates"
+  },
+  {
+   "metric_id": "receipts.business_rates",
+   "year": 2029,
+   "value": 31732494964.235413,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "business_rates",
+   "label": "Business rates"
+  },
+  {
+   "metric_id": "receipts.business_rates",
+   "year": 2030,
+   "value": 31732498638.635994,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "business_rates",
+   "label": "Business rates"
+  },
+  {
+   "metric_id": "receipts.business_rates",
+   "year": 2031,
+   "value": 31732494471.035408,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "business_rates",
+   "label": "Business rates"
+  },
+  {
+   "metric_id": "receipts.sdlt_household",
+   "year": 2025,
+   "value": 19073759714.657246,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "sdlt_household",
+   "label": "Stamp duty land tax (own purchases, and corporate until the"
+  },
+  {
+   "metric_id": "receipts.sdlt_household",
+   "year": 2026,
+   "value": 20077204232.549347,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "sdlt_household",
+   "label": "Stamp duty land tax (own purchases, and corporate until the"
+  },
+  {
+   "metric_id": "receipts.sdlt_household",
+   "year": 2027,
+   "value": 21035871774.44113,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "sdlt_household",
+   "label": "Stamp duty land tax (own purchases, and corporate until the"
+  },
+  {
+   "metric_id": "receipts.sdlt_household",
+   "year": 2028,
+   "value": 21999031263.396732,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "sdlt_household",
+   "label": "Stamp duty land tax (own purchases, and corporate until the"
+  },
+  {
+   "metric_id": "receipts.sdlt_household",
+   "year": 2029,
+   "value": 22985619104.754326,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "sdlt_household",
+   "label": "Stamp duty land tax (own purchases, and corporate until the"
+  },
+  {
+   "metric_id": "receipts.sdlt_household",
+   "year": 2030,
+   "value": 24063362268.549835,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "sdlt_household",
+   "label": "Stamp duty land tax (own purchases, and corporate until the"
+  },
+  {
+   "metric_id": "receipts.sdlt_household",
+   "year": 2031,
+   "value": 25218986028.960094,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "sdlt_household",
+   "label": "Stamp duty land tax (own purchases, and corporate until the"
+  },
+  {
+   "metric_id": "receipts.land_transaction_tax",
+   "year": 2025,
+   "value": 3441794.3282832364,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_transaction_tax",
+   "label": "Land transaction tax (Wales)"
+  },
+  {
+   "metric_id": "receipts.land_transaction_tax",
+   "year": 2026,
+   "value": 3718451.781090757,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_transaction_tax",
+   "label": "Land transaction tax (Wales)"
+  },
+  {
+   "metric_id": "receipts.land_transaction_tax",
+   "year": 2027,
+   "value": 4036378.263004425,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_transaction_tax",
+   "label": "Land transaction tax (Wales)"
+  },
+  {
+   "metric_id": "receipts.land_transaction_tax",
+   "year": 2028,
+   "value": 4360394.001039479,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_transaction_tax",
+   "label": "Land transaction tax (Wales)"
+  },
+  {
+   "metric_id": "receipts.land_transaction_tax",
+   "year": 2029,
+   "value": 4685725.303715903,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_transaction_tax",
+   "label": "Land transaction tax (Wales)"
+  },
+  {
+   "metric_id": "receipts.land_transaction_tax",
+   "year": 2030,
+   "value": 5044918.447127742,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_transaction_tax",
+   "label": "Land transaction tax (Wales)"
+  },
+  {
+   "metric_id": "receipts.land_transaction_tax",
+   "year": 2031,
+   "value": 5422944.119211734,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_transaction_tax",
+   "label": "Land transaction tax (Wales)"
+  },
+  {
+   "metric_id": "receipts.land_and_buildings_transaction_tax",
+   "year": 2025,
+   "value": 109691161.84922537,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_and_buildings_transaction_tax",
+   "label": "Land and buildings transaction tax (Scotland)"
+  },
+  {
+   "metric_id": "receipts.land_and_buildings_transaction_tax",
+   "year": 2026,
+   "value": 114611758.83902286,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_and_buildings_transaction_tax",
+   "label": "Land and buildings transaction tax (Scotland)"
+  },
+  {
+   "metric_id": "receipts.land_and_buildings_transaction_tax",
+   "year": 2027,
+   "value": 120242919.16397013,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_and_buildings_transaction_tax",
+   "label": "Land and buildings transaction tax (Scotland)"
+  },
+  {
+   "metric_id": "receipts.land_and_buildings_transaction_tax",
+   "year": 2028,
+   "value": 125932263.95004636,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_and_buildings_transaction_tax",
+   "label": "Land and buildings transaction tax (Scotland)"
+  },
+  {
+   "metric_id": "receipts.land_and_buildings_transaction_tax",
+   "year": 2029,
+   "value": 131640517.58306551,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_and_buildings_transaction_tax",
+   "label": "Land and buildings transaction tax (Scotland)"
+  },
+  {
+   "metric_id": "receipts.land_and_buildings_transaction_tax",
+   "year": 2030,
+   "value": 137912689.4621755,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_and_buildings_transaction_tax",
+   "label": "Land and buildings transaction tax (Scotland)"
+  },
+  {
+   "metric_id": "receipts.land_and_buildings_transaction_tax",
+   "year": 2031,
+   "value": 144400087.45167437,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_and_buildings_transaction_tax",
+   "label": "Land and buildings transaction tax (Scotland)"
+  },
+  {
+   "metric_id": "receipts.corporate_incident_tax_change",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "corporate_incident_tax_change",
+   "label": "Unspecified tax change borne by capital"
+  },
+  {
+   "metric_id": "receipts.corporate_incident_tax_change",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "corporate_incident_tax_change",
+   "label": "Unspecified tax change borne by capital"
+  },
+  {
+   "metric_id": "receipts.corporate_incident_tax_change",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "corporate_incident_tax_change",
+   "label": "Unspecified tax change borne by capital"
+  },
+  {
+   "metric_id": "receipts.corporate_incident_tax_change",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "corporate_incident_tax_change",
+   "label": "Unspecified tax change borne by capital"
+  },
+  {
+   "metric_id": "receipts.corporate_incident_tax_change",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "corporate_incident_tax_change",
+   "label": "Unspecified tax change borne by capital"
+  },
+  {
+   "metric_id": "receipts.corporate_incident_tax_change",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "corporate_incident_tax_change",
+   "label": "Unspecified tax change borne by capital"
+  },
+  {
+   "metric_id": "receipts.corporate_incident_tax_change",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "corporate_incident_tax_change",
+   "label": "Unspecified tax change borne by capital"
+  },
+  {
+   "metric_id": "receipts.consumer_incident_tax_change",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "consumer_incident_tax_change",
+   "label": "Unspecified tax change borne by consumers"
+  },
+  {
+   "metric_id": "receipts.consumer_incident_tax_change",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "consumer_incident_tax_change",
+   "label": "Unspecified tax change borne by consumers"
+  },
+  {
+   "metric_id": "receipts.consumer_incident_tax_change",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "consumer_incident_tax_change",
+   "label": "Unspecified tax change borne by consumers"
+  },
+  {
+   "metric_id": "receipts.consumer_incident_tax_change",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "consumer_incident_tax_change",
+   "label": "Unspecified tax change borne by consumers"
+  },
+  {
+   "metric_id": "receipts.consumer_incident_tax_change",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "consumer_incident_tax_change",
+   "label": "Unspecified tax change borne by consumers"
+  },
+  {
+   "metric_id": "receipts.consumer_incident_tax_change",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "consumer_incident_tax_change",
+   "label": "Unspecified tax change borne by consumers"
+  },
+  {
+   "metric_id": "receipts.consumer_incident_tax_change",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "consumer_incident_tax_change",
+   "label": "Unspecified tax change borne by consumers"
+  },
+  {
+   "metric_id": "receipts.wealth_tax",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "wealth_tax",
+   "label": "Wealth tax"
+  },
+  {
+   "metric_id": "receipts.wealth_tax",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "wealth_tax",
+   "label": "Wealth tax"
+  },
+  {
+   "metric_id": "receipts.wealth_tax",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "wealth_tax",
+   "label": "Wealth tax"
+  },
+  {
+   "metric_id": "receipts.wealth_tax",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "wealth_tax",
+   "label": "Wealth tax"
+  },
+  {
+   "metric_id": "receipts.wealth_tax",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "wealth_tax",
+   "label": "Wealth tax"
+  },
+  {
+   "metric_id": "receipts.wealth_tax",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "wealth_tax",
+   "label": "Wealth tax"
+  },
+  {
+   "metric_id": "receipts.wealth_tax",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "wealth_tax",
+   "label": "Wealth tax"
+  },
+  {
+   "metric_id": "receipts.non_primary_residence_wealth_tax",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "non_primary_residence_wealth_tax",
+   "label": "Wealth tax excluding the main residence"
+  },
+  {
+   "metric_id": "receipts.non_primary_residence_wealth_tax",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "non_primary_residence_wealth_tax",
+   "label": "Wealth tax excluding the main residence"
+  },
+  {
+   "metric_id": "receipts.non_primary_residence_wealth_tax",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "non_primary_residence_wealth_tax",
+   "label": "Wealth tax excluding the main residence"
+  },
+  {
+   "metric_id": "receipts.non_primary_residence_wealth_tax",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "non_primary_residence_wealth_tax",
+   "label": "Wealth tax excluding the main residence"
+  },
+  {
+   "metric_id": "receipts.non_primary_residence_wealth_tax",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "non_primary_residence_wealth_tax",
+   "label": "Wealth tax excluding the main residence"
+  },
+  {
+   "metric_id": "receipts.non_primary_residence_wealth_tax",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "non_primary_residence_wealth_tax",
+   "label": "Wealth tax excluding the main residence"
+  },
+  {
+   "metric_id": "receipts.non_primary_residence_wealth_tax",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "non_primary_residence_wealth_tax",
+   "label": "Wealth tax excluding the main residence"
+  },
+  {
+   "metric_id": "receipts.land_value_tax",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_value_tax",
+   "label": "Land value tax"
+  },
+  {
+   "metric_id": "receipts.land_value_tax",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_value_tax",
+   "label": "Land value tax"
+  },
+  {
+   "metric_id": "receipts.land_value_tax",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_value_tax",
+   "label": "Land value tax"
+  },
+  {
+   "metric_id": "receipts.land_value_tax",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_value_tax",
+   "label": "Land value tax"
+  },
+  {
+   "metric_id": "receipts.land_value_tax",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_value_tax",
+   "label": "Land value tax"
+  },
+  {
+   "metric_id": "receipts.land_value_tax",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_value_tax",
+   "label": "Land value tax"
+  },
+  {
+   "metric_id": "receipts.land_value_tax",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "land_value_tax",
+   "label": "Land value tax"
+  },
+  {
+   "metric_id": "receipts.carbon_tax",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "carbon_tax",
+   "label": "Carbon tax"
+  },
+  {
+   "metric_id": "receipts.carbon_tax",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "carbon_tax",
+   "label": "Carbon tax"
+  },
+  {
+   "metric_id": "receipts.carbon_tax",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "carbon_tax",
+   "label": "Carbon tax"
+  },
+  {
+   "metric_id": "receipts.carbon_tax",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "carbon_tax",
+   "label": "Carbon tax"
+  },
+  {
+   "metric_id": "receipts.carbon_tax",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "carbon_tax",
+   "label": "Carbon tax"
+  },
+  {
+   "metric_id": "receipts.carbon_tax",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "carbon_tax",
+   "label": "Carbon tax"
+  },
+  {
+   "metric_id": "receipts.carbon_tax",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "receipt",
+   "partition": "carbon_tax",
+   "label": "Carbon tax"
+  },
+  {
+   "metric_id": "spending.state_pension",
+   "year": 2025,
+   "value": 122503036176.28136,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "state_pension",
+   "label": "State Pension"
+  },
+  {
+   "metric_id": "spending.state_pension",
+   "year": 2026,
+   "value": 127115003712.11774,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "state_pension",
+   "label": "State Pension"
+  },
+  {
+   "metric_id": "spending.state_pension",
+   "year": 2027,
+   "value": 128280611587.4219,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "state_pension",
+   "label": "State Pension"
+  },
+  {
+   "metric_id": "spending.state_pension",
+   "year": 2028,
+   "value": 130209285651.49004,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "state_pension",
+   "label": "State Pension"
+  },
+  {
+   "metric_id": "spending.state_pension",
+   "year": 2029,
+   "value": 134051705144.42847,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "state_pension",
+   "label": "State Pension"
+  },
+  {
+   "metric_id": "spending.state_pension",
+   "year": 2030,
+   "value": 138021583333.92395,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "state_pension",
+   "label": "State Pension"
+  },
+  {
+   "metric_id": "spending.state_pension",
+   "year": 2031,
+   "value": 142080411583.76672,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "state_pension",
+   "label": "State Pension"
+  },
+  {
+   "metric_id": "caseloads.state_pension",
+   "year": 2025,
+   "value": 11307942.897278756,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "state_pension",
+   "label": "State Pension"
+  },
+  {
+   "metric_id": "caseloads.state_pension",
+   "year": 2026,
+   "value": 11208358.708708271,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "state_pension",
+   "label": "State Pension"
+  },
+  {
+   "metric_id": "caseloads.state_pension",
+   "year": 2027,
+   "value": 10906221.550106525,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "state_pension",
+   "label": "State Pension"
+  },
+  {
+   "metric_id": "caseloads.state_pension",
+   "year": 2028,
+   "value": 10811938.301638946,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "state_pension",
+   "label": "State Pension"
+  },
+  {
+   "metric_id": "caseloads.state_pension",
+   "year": 2029,
+   "value": 10859510.9701875,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "state_pension",
+   "label": "State Pension"
+  },
+  {
+   "metric_id": "caseloads.state_pension",
+   "year": 2030,
+   "value": 10908379.084595531,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "state_pension",
+   "label": "State Pension"
+  },
+  {
+   "metric_id": "caseloads.state_pension",
+   "year": 2031,
+   "value": 10955285.089603484,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "state_pension",
+   "label": "State Pension"
+  },
+  {
+   "metric_id": "spending.pension_credit",
+   "year": 2025,
+   "value": 6683600257.719555,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pension_credit",
+   "label": "Pension Credit"
+  },
+  {
+   "metric_id": "spending.pension_credit",
+   "year": 2026,
+   "value": 7056299099.336407,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pension_credit",
+   "label": "Pension Credit"
+  },
+  {
+   "metric_id": "spending.pension_credit",
+   "year": 2027,
+   "value": 7040775597.466119,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pension_credit",
+   "label": "Pension Credit"
+  },
+  {
+   "metric_id": "spending.pension_credit",
+   "year": 2028,
+   "value": 7110102297.589521,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pension_credit",
+   "label": "Pension Credit"
+  },
+  {
+   "metric_id": "spending.pension_credit",
+   "year": 2029,
+   "value": 7231026514.317166,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pension_credit",
+   "label": "Pension Credit"
+  },
+  {
+   "metric_id": "spending.pension_credit",
+   "year": 2030,
+   "value": 7365763489.682707,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pension_credit",
+   "label": "Pension Credit"
+  },
+  {
+   "metric_id": "spending.pension_credit",
+   "year": 2031,
+   "value": 7463303552.117271,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pension_credit",
+   "label": "Pension Credit"
+  },
+  {
+   "metric_id": "caseloads.pension_credit",
+   "year": 2025,
+   "value": 1490774.5672746152,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pension_credit",
+   "label": "Pension Credit"
+  },
+  {
+   "metric_id": "caseloads.pension_credit",
+   "year": 2026,
+   "value": 1489116.733679533,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pension_credit",
+   "label": "Pension Credit"
+  },
+  {
+   "metric_id": "caseloads.pension_credit",
+   "year": 2027,
+   "value": 1477429.6021682024,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pension_credit",
+   "label": "Pension Credit"
+  },
+  {
+   "metric_id": "caseloads.pension_credit",
+   "year": 2028,
+   "value": 1455339.335629344,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pension_credit",
+   "label": "Pension Credit"
+  },
+  {
+   "metric_id": "caseloads.pension_credit",
+   "year": 2029,
+   "value": 1457221.2425712496,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pension_credit",
+   "label": "Pension Credit"
+  },
+  {
+   "metric_id": "caseloads.pension_credit",
+   "year": 2030,
+   "value": 1454060.4666527808,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pension_credit",
+   "label": "Pension Credit"
+  },
+  {
+   "metric_id": "caseloads.pension_credit",
+   "year": 2031,
+   "value": 1450518.8676682264,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pension_credit",
+   "label": "Pension Credit"
+  },
+  {
+   "metric_id": "spending.winter_fuel_payment",
+   "year": 2025,
+   "value": 1696782924.1721363,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "winter_fuel_payment",
+   "label": "Winter Fuel Payment"
+  },
+  {
+   "metric_id": "spending.winter_fuel_payment",
+   "year": 2026,
+   "value": 1679283343.732223,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "winter_fuel_payment",
+   "label": "Winter Fuel Payment"
+  },
+  {
+   "metric_id": "spending.winter_fuel_payment",
+   "year": 2027,
+   "value": 1590003738.0768867,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "winter_fuel_payment",
+   "label": "Winter Fuel Payment"
+  },
+  {
+   "metric_id": "spending.winter_fuel_payment",
+   "year": 2028,
+   "value": 1573027080.7015598,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "winter_fuel_payment",
+   "label": "Winter Fuel Payment"
+  },
+  {
+   "metric_id": "spending.winter_fuel_payment",
+   "year": 2029,
+   "value": 1569952442.020166,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "winter_fuel_payment",
+   "label": "Winter Fuel Payment"
+  },
+  {
+   "metric_id": "spending.winter_fuel_payment",
+   "year": 2030,
+   "value": 1547501174.76131,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "winter_fuel_payment",
+   "label": "Winter Fuel Payment"
+  },
+  {
+   "metric_id": "spending.winter_fuel_payment",
+   "year": 2031,
+   "value": 1519503572.7274477,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "winter_fuel_payment",
+   "label": "Winter Fuel Payment"
+  },
+  {
+   "metric_id": "caseloads.winter_fuel_payment",
+   "year": 2025,
+   "value": 7090970.121831879,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "winter_fuel_payment",
+   "label": "Winter Fuel Payment"
+  },
+  {
+   "metric_id": "caseloads.winter_fuel_payment",
+   "year": 2026,
+   "value": 7007600.1749506295,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "winter_fuel_payment",
+   "label": "Winter Fuel Payment"
+  },
+  {
+   "metric_id": "caseloads.winter_fuel_payment",
+   "year": 2027,
+   "value": 6593488.894350678,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "winter_fuel_payment",
+   "label": "Winter Fuel Payment"
+  },
+  {
+   "metric_id": "caseloads.winter_fuel_payment",
+   "year": 2028,
+   "value": 6508639.181520969,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "winter_fuel_payment",
+   "label": "Winter Fuel Payment"
+  },
+  {
+   "metric_id": "caseloads.winter_fuel_payment",
+   "year": 2029,
+   "value": 6490585.181142107,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "winter_fuel_payment",
+   "label": "Winter Fuel Payment"
+  },
+  {
+   "metric_id": "caseloads.winter_fuel_payment",
+   "year": 2030,
+   "value": 6377424.451988548,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "winter_fuel_payment",
+   "label": "Winter Fuel Payment"
+  },
+  {
+   "metric_id": "caseloads.winter_fuel_payment",
+   "year": 2031,
+   "value": 6242475.78672789,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "winter_fuel_payment",
+   "label": "Winter Fuel Payment"
+  },
+  {
+   "metric_id": "spending.pawhp",
+   "year": 2025,
+   "value": 190004357.0627768,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pawhp",
+   "label": "Pension Age Winter Heating Payment (Scotland)"
+  },
+  {
+   "metric_id": "spending.pawhp",
+   "year": 2026,
+   "value": 197376523.34808874,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pawhp",
+   "label": "Pension Age Winter Heating Payment (Scotland)"
+  },
+  {
+   "metric_id": "spending.pawhp",
+   "year": 2027,
+   "value": 201464846.70418116,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pawhp",
+   "label": "Pension Age Winter Heating Payment (Scotland)"
+  },
+  {
+   "metric_id": "spending.pawhp",
+   "year": 2028,
+   "value": 205237714.89489964,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pawhp",
+   "label": "Pension Age Winter Heating Payment (Scotland)"
+  },
+  {
+   "metric_id": "spending.pawhp",
+   "year": 2029,
+   "value": 210263123.1305548,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pawhp",
+   "label": "Pension Age Winter Heating Payment (Scotland)"
+  },
+  {
+   "metric_id": "spending.pawhp",
+   "year": 2030,
+   "value": 215433546.71501532,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pawhp",
+   "label": "Pension Age Winter Heating Payment (Scotland)"
+  },
+  {
+   "metric_id": "spending.pawhp",
+   "year": 2031,
+   "value": 220687250.38609877,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pawhp",
+   "label": "Pension Age Winter Heating Payment (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.pawhp",
+   "year": 2025,
+   "value": 806440.3659721613,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pawhp",
+   "label": "Pension Age Winter Heating Payment (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.pawhp",
+   "year": 2026,
+   "value": 806642.316529274,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pawhp",
+   "label": "Pension Age Winter Heating Payment (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.pawhp",
+   "year": 2027,
+   "value": 806823.3181906939,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pawhp",
+   "label": "Pension Age Winter Heating Payment (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.pawhp",
+   "year": 2028,
+   "value": 805142.1836649776,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pawhp",
+   "label": "Pension Age Winter Heating Payment (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.pawhp",
+   "year": 2029,
+   "value": 808684.8276723027,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pawhp",
+   "label": "Pension Age Winter Heating Payment (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.pawhp",
+   "year": 2030,
+   "value": 812323.9398192763,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pawhp",
+   "label": "Pension Age Winter Heating Payment (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.pawhp",
+   "year": 2031,
+   "value": 815816.9233164787,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pawhp",
+   "label": "Pension Age Winter Heating Payment (Scotland)"
+  },
+  {
+   "metric_id": "spending.universal_credit",
+   "year": 2025,
+   "value": 79166098634.76385,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "universal_credit",
+   "label": "Universal Credit"
+  },
+  {
+   "metric_id": "spending.universal_credit",
+   "year": 2026,
+   "value": 83483281466.41425,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "universal_credit",
+   "label": "Universal Credit"
+  },
+  {
+   "metric_id": "spending.universal_credit",
+   "year": 2027,
+   "value": 85110190875.9473,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "universal_credit",
+   "label": "Universal Credit"
+  },
+  {
+   "metric_id": "spending.universal_credit",
+   "year": 2028,
+   "value": 86667339883.95622,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "universal_credit",
+   "label": "Universal Credit"
+  },
+  {
+   "metric_id": "spending.universal_credit",
+   "year": 2029,
+   "value": 87402948162.99124,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "universal_credit",
+   "label": "Universal Credit"
+  },
+  {
+   "metric_id": "spending.universal_credit",
+   "year": 2030,
+   "value": 84214027875.53,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "universal_credit",
+   "label": "Universal Credit"
+  },
+  {
+   "metric_id": "spending.universal_credit",
+   "year": 2031,
+   "value": 85457636655.59526,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "universal_credit",
+   "label": "Universal Credit"
+  },
+  {
+   "metric_id": "caseloads.universal_credit",
+   "year": 2025,
+   "value": 6371587.519835666,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "universal_credit",
+   "label": "Universal Credit"
+  },
+  {
+   "metric_id": "caseloads.universal_credit",
+   "year": 2026,
+   "value": 6379016.747252494,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "universal_credit",
+   "label": "Universal Credit"
+  },
+  {
+   "metric_id": "caseloads.universal_credit",
+   "year": 2027,
+   "value": 6394538.736335561,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "universal_credit",
+   "label": "Universal Credit"
+  },
+  {
+   "metric_id": "caseloads.universal_credit",
+   "year": 2028,
+   "value": 6399621.182661429,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "universal_credit",
+   "label": "Universal Credit"
+  },
+  {
+   "metric_id": "caseloads.universal_credit",
+   "year": 2029,
+   "value": 6373153.488585249,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "universal_credit",
+   "label": "Universal Credit"
+  },
+  {
+   "metric_id": "caseloads.universal_credit",
+   "year": 2030,
+   "value": 6316123.800888762,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "universal_credit",
+   "label": "Universal Credit"
+  },
+  {
+   "metric_id": "caseloads.universal_credit",
+   "year": 2031,
+   "value": 6311765.528904498,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "universal_credit",
+   "label": "Universal Credit"
+  },
+  {
+   "metric_id": "spending.housing_benefit",
+   "year": 2025,
+   "value": 8595335171.520943,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "housing_benefit",
+   "label": "Housing Benefit"
+  },
+  {
+   "metric_id": "spending.housing_benefit",
+   "year": 2026,
+   "value": 8931864198.577654,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "housing_benefit",
+   "label": "Housing Benefit"
+  },
+  {
+   "metric_id": "spending.housing_benefit",
+   "year": 2027,
+   "value": 9199477872.067867,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "housing_benefit",
+   "label": "Housing Benefit"
+  },
+  {
+   "metric_id": "spending.housing_benefit",
+   "year": 2028,
+   "value": 9343759945.943094,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "housing_benefit",
+   "label": "Housing Benefit"
+  },
+  {
+   "metric_id": "spending.housing_benefit",
+   "year": 2029,
+   "value": 9621753903.580982,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "housing_benefit",
+   "label": "Housing Benefit"
+  },
+  {
+   "metric_id": "spending.housing_benefit",
+   "year": 2030,
+   "value": 9883322170.08307,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "housing_benefit",
+   "label": "Housing Benefit"
+  },
+  {
+   "metric_id": "spending.housing_benefit",
+   "year": 2031,
+   "value": 10165697938.801252,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "housing_benefit",
+   "label": "Housing Benefit"
+  },
+  {
+   "metric_id": "caseloads.housing_benefit",
+   "year": 2025,
+   "value": 1254488.0164678842,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "housing_benefit",
+   "label": "Housing Benefit"
+  },
+  {
+   "metric_id": "caseloads.housing_benefit",
+   "year": 2026,
+   "value": 1252740.275393933,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "housing_benefit",
+   "label": "Housing Benefit"
+  },
+  {
+   "metric_id": "caseloads.housing_benefit",
+   "year": 2027,
+   "value": 1254335.1581986696,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "housing_benefit",
+   "label": "Housing Benefit"
+  },
+  {
+   "metric_id": "caseloads.housing_benefit",
+   "year": 2028,
+   "value": 1240028.0728783756,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "housing_benefit",
+   "label": "Housing Benefit"
+  },
+  {
+   "metric_id": "caseloads.housing_benefit",
+   "year": 2029,
+   "value": 1244626.769205615,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "housing_benefit",
+   "label": "Housing Benefit"
+  },
+  {
+   "metric_id": "caseloads.housing_benefit",
+   "year": 2030,
+   "value": 1246801.715999484,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "housing_benefit",
+   "label": "Housing Benefit"
+  },
+  {
+   "metric_id": "caseloads.housing_benefit",
+   "year": 2031,
+   "value": 1249258.3732767105,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "housing_benefit",
+   "label": "Housing Benefit"
+  },
+  {
+   "metric_id": "spending.council_tax_reduction",
+   "year": 2025,
+   "value": 1988515658.8280945,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "council_tax_reduction",
+   "label": "Council Tax Reduction"
+  },
+  {
+   "metric_id": "spending.council_tax_reduction",
+   "year": 2026,
+   "value": 2033885987.1873639,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "council_tax_reduction",
+   "label": "Council Tax Reduction"
+  },
+  {
+   "metric_id": "spending.council_tax_reduction",
+   "year": 2027,
+   "value": 2079220973.3377244,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "council_tax_reduction",
+   "label": "Council Tax Reduction"
+  },
+  {
+   "metric_id": "spending.council_tax_reduction",
+   "year": 2028,
+   "value": 2130392978.7542346,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "council_tax_reduction",
+   "label": "Council Tax Reduction"
+  },
+  {
+   "metric_id": "spending.council_tax_reduction",
+   "year": 2029,
+   "value": 2182371431.2323685,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "council_tax_reduction",
+   "label": "Council Tax Reduction"
+  },
+  {
+   "metric_id": "spending.council_tax_reduction",
+   "year": 2030,
+   "value": 2245332431.4065833,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "council_tax_reduction",
+   "label": "Council Tax Reduction"
+  },
+  {
+   "metric_id": "spending.council_tax_reduction",
+   "year": 2031,
+   "value": 2299546088.50543,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "council_tax_reduction",
+   "label": "Council Tax Reduction"
+  },
+  {
+   "metric_id": "caseloads.council_tax_reduction",
+   "year": 2025,
+   "value": 3187241.3228853345,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "council_tax_reduction",
+   "label": "Council Tax Reduction"
+  },
+  {
+   "metric_id": "caseloads.council_tax_reduction",
+   "year": 2026,
+   "value": 3211653.8483200073,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "council_tax_reduction",
+   "label": "Council Tax Reduction"
+  },
+  {
+   "metric_id": "caseloads.council_tax_reduction",
+   "year": 2027,
+   "value": 3226461.487433046,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "council_tax_reduction",
+   "label": "Council Tax Reduction"
+  },
+  {
+   "metric_id": "caseloads.council_tax_reduction",
+   "year": 2028,
+   "value": 3249134.5323915184,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "council_tax_reduction",
+   "label": "Council Tax Reduction"
+  },
+  {
+   "metric_id": "caseloads.council_tax_reduction",
+   "year": 2029,
+   "value": 3264581.2450436354,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "council_tax_reduction",
+   "label": "Council Tax Reduction"
+  },
+  {
+   "metric_id": "caseloads.council_tax_reduction",
+   "year": 2030,
+   "value": 3296161.365950793,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "council_tax_reduction",
+   "label": "Council Tax Reduction"
+  },
+  {
+   "metric_id": "caseloads.council_tax_reduction",
+   "year": 2031,
+   "value": 3310563.1543370783,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "council_tax_reduction",
+   "label": "Council Tax Reduction"
+  },
+  {
+   "metric_id": "spending.esa_income",
+   "year": 2025,
+   "value": 2119219755.27102,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "esa_income",
+   "label": "Employment and Support Allowance (income-related)"
+  },
+  {
+   "metric_id": "spending.esa_income",
+   "year": 2026,
+   "value": 2175740626.0724893,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "esa_income",
+   "label": "Employment and Support Allowance (income-related)"
+  },
+  {
+   "metric_id": "spending.esa_income",
+   "year": 2027,
+   "value": 2226624100.582522,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "esa_income",
+   "label": "Employment and Support Allowance (income-related)"
+  },
+  {
+   "metric_id": "spending.esa_income",
+   "year": 2028,
+   "value": 2277324834.952569,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "esa_income",
+   "label": "Employment and Support Allowance (income-related)"
+  },
+  {
+   "metric_id": "spending.esa_income",
+   "year": 2029,
+   "value": 2332267574.159017,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "esa_income",
+   "label": "Employment and Support Allowance (income-related)"
+  },
+  {
+   "metric_id": "spending.esa_income",
+   "year": 2030,
+   "value": 2388837175.037817,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "esa_income",
+   "label": "Employment and Support Allowance (income-related)"
+  },
+  {
+   "metric_id": "spending.esa_income",
+   "year": 2031,
+   "value": 2446074707.6473985,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "esa_income",
+   "label": "Employment and Support Allowance (income-related)"
+  },
+  {
+   "metric_id": "caseloads.esa_income",
+   "year": 2025,
+   "value": 186830.98939606547,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "esa_income",
+   "label": "Employment and Support Allowance (income-related)"
+  },
+  {
+   "metric_id": "caseloads.esa_income",
+   "year": 2026,
+   "value": 187540.95343746245,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "esa_income",
+   "label": "Employment and Support Allowance (income-related)"
+  },
+  {
+   "metric_id": "caseloads.esa_income",
+   "year": 2027,
+   "value": 188234.8581573367,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "esa_income",
+   "label": "Employment and Support Allowance (income-related)"
+  },
+  {
+   "metric_id": "caseloads.esa_income",
+   "year": 2028,
+   "value": 188797.70078976452,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "esa_income",
+   "label": "Employment and Support Allowance (income-related)"
+  },
+  {
+   "metric_id": "caseloads.esa_income",
+   "year": 2029,
+   "value": 189628.41371196508,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "esa_income",
+   "label": "Employment and Support Allowance (income-related)"
+  },
+  {
+   "metric_id": "caseloads.esa_income",
+   "year": 2030,
+   "value": 190481.74765598774,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "esa_income",
+   "label": "Employment and Support Allowance (income-related)"
+  },
+  {
+   "metric_id": "caseloads.esa_income",
+   "year": 2031,
+   "value": 191300.81845253706,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "esa_income",
+   "label": "Employment and Support Allowance (income-related)"
+  },
+  {
+   "metric_id": "spending.esa_contrib",
+   "year": 2025,
+   "value": 5221642514.164815,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "esa_contrib",
+   "label": "Employment and Support Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "spending.esa_contrib",
+   "year": 2026,
+   "value": 5362038966.389469,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "esa_contrib",
+   "label": "Employment and Support Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "spending.esa_contrib",
+   "year": 2027,
+   "value": 5489516286.663287,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "esa_contrib",
+   "label": "Employment and Support Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "spending.esa_contrib",
+   "year": 2028,
+   "value": 5621703470.566074,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "esa_contrib",
+   "label": "Employment and Support Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "spending.esa_contrib",
+   "year": 2029,
+   "value": 5759367828.016521,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "esa_contrib",
+   "label": "Employment and Support Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "spending.esa_contrib",
+   "year": 2030,
+   "value": 5900990569.133743,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "esa_contrib",
+   "label": "Employment and Support Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "spending.esa_contrib",
+   "year": 2031,
+   "value": 6044892357.516051,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "esa_contrib",
+   "label": "Employment and Support Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "caseloads.esa_contrib",
+   "year": 2025,
+   "value": 721684.3135545999,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "esa_contrib",
+   "label": "Employment and Support Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "caseloads.esa_contrib",
+   "year": 2026,
+   "value": 724426.7398380041,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "esa_contrib",
+   "label": "Employment and Support Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "caseloads.esa_contrib",
+   "year": 2027,
+   "value": 727107.1310208887,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "esa_contrib",
+   "label": "Employment and Support Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "caseloads.esa_contrib",
+   "year": 2028,
+   "value": 730015.5208309293,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "esa_contrib",
+   "label": "Employment and Support Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "caseloads.esa_contrib",
+   "year": 2029,
+   "value": 733227.5983880013,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "esa_contrib",
+   "label": "Employment and Support Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "caseloads.esa_contrib",
+   "year": 2030,
+   "value": 736527.1409217119,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "esa_contrib",
+   "label": "Employment and Support Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "caseloads.esa_contrib",
+   "year": 2031,
+   "value": 739694.2040582746,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "esa_contrib",
+   "label": "Employment and Support Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "spending.jsa_income",
+   "year": 2025,
+   "value": 1446979.5182146362,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "jsa_income",
+   "label": "Jobseeker's Allowance (income-based)"
+  },
+  {
+   "metric_id": "spending.jsa_income",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "jsa_income",
+   "label": "Jobseeker's Allowance (income-based)"
+  },
+  {
+   "metric_id": "spending.jsa_income",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "jsa_income",
+   "label": "Jobseeker's Allowance (income-based)"
+  },
+  {
+   "metric_id": "spending.jsa_income",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "jsa_income",
+   "label": "Jobseeker's Allowance (income-based)"
+  },
+  {
+   "metric_id": "spending.jsa_income",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "jsa_income",
+   "label": "Jobseeker's Allowance (income-based)"
+  },
+  {
+   "metric_id": "spending.jsa_income",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "jsa_income",
+   "label": "Jobseeker's Allowance (income-based)"
+  },
+  {
+   "metric_id": "spending.jsa_income",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "jsa_income",
+   "label": "Jobseeker's Allowance (income-based)"
+  },
+  {
+   "metric_id": "caseloads.jsa_income",
+   "year": 2025,
+   "value": 278.16873490810394,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "jsa_income",
+   "label": "Jobseeker's Allowance (income-based)"
+  },
+  {
+   "metric_id": "caseloads.jsa_income",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "jsa_income",
+   "label": "Jobseeker's Allowance (income-based)"
+  },
+  {
+   "metric_id": "caseloads.jsa_income",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "jsa_income",
+   "label": "Jobseeker's Allowance (income-based)"
+  },
+  {
+   "metric_id": "caseloads.jsa_income",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "jsa_income",
+   "label": "Jobseeker's Allowance (income-based)"
+  },
+  {
+   "metric_id": "caseloads.jsa_income",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "jsa_income",
+   "label": "Jobseeker's Allowance (income-based)"
+  },
+  {
+   "metric_id": "caseloads.jsa_income",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "jsa_income",
+   "label": "Jobseeker's Allowance (income-based)"
+  },
+  {
+   "metric_id": "caseloads.jsa_income",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "jsa_income",
+   "label": "Jobseeker's Allowance (income-based)"
+  },
+  {
+   "metric_id": "spending.jsa_contrib",
+   "year": 2025,
+   "value": 288777970.5356167,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "jsa_contrib",
+   "label": "Jobseeker's Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "spending.jsa_contrib",
+   "year": 2026,
+   "value": 296542457.71586204,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "jsa_contrib",
+   "label": "Jobseeker's Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "spending.jsa_contrib",
+   "year": 2027,
+   "value": 303592471.1494838,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "jsa_contrib",
+   "label": "Jobseeker's Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "spending.jsa_contrib",
+   "year": 2028,
+   "value": 310902955.13914746,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "jsa_contrib",
+   "label": "Jobseeker's Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "spending.jsa_contrib",
+   "year": 2029,
+   "value": 318516348.7643643,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "jsa_contrib",
+   "label": "Jobseeker's Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "spending.jsa_contrib",
+   "year": 2030,
+   "value": 326348673.79918647,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "jsa_contrib",
+   "label": "Jobseeker's Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "spending.jsa_contrib",
+   "year": 2031,
+   "value": 334307010.21562654,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "jsa_contrib",
+   "label": "Jobseeker's Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "caseloads.jsa_contrib",
+   "year": 2025,
+   "value": 67182.8121483326,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "jsa_contrib",
+   "label": "Jobseeker's Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "caseloads.jsa_contrib",
+   "year": 2026,
+   "value": 67438.10866451263,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "jsa_contrib",
+   "label": "Jobseeker's Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "caseloads.jsa_contrib",
+   "year": 2027,
+   "value": 67687.63075757027,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "jsa_contrib",
+   "label": "Jobseeker's Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "caseloads.jsa_contrib",
+   "year": 2028,
+   "value": 67958.37626743317,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "jsa_contrib",
+   "label": "Jobseeker's Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "caseloads.jsa_contrib",
+   "year": 2029,
+   "value": 68257.39367341995,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "jsa_contrib",
+   "label": "Jobseeker's Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "caseloads.jsa_contrib",
+   "year": 2030,
+   "value": 68564.55532884598,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "jsa_contrib",
+   "label": "Jobseeker's Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "caseloads.jsa_contrib",
+   "year": 2031,
+   "value": 68859.38249921799,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "jsa_contrib",
+   "label": "Jobseeker's Allowance (contribution-based)"
+  },
+  {
+   "metric_id": "spending.income_support",
+   "year": 2025,
+   "value": 123955194.70721354,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "income_support",
+   "label": "Income Support"
+  },
+  {
+   "metric_id": "spending.income_support",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "income_support",
+   "label": "Income Support"
+  },
+  {
+   "metric_id": "spending.income_support",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "income_support",
+   "label": "Income Support"
+  },
+  {
+   "metric_id": "spending.income_support",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "income_support",
+   "label": "Income Support"
+  },
+  {
+   "metric_id": "spending.income_support",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "income_support",
+   "label": "Income Support"
+  },
+  {
+   "metric_id": "spending.income_support",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "income_support",
+   "label": "Income Support"
+  },
+  {
+   "metric_id": "spending.income_support",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "income_support",
+   "label": "Income Support"
+  },
+  {
+   "metric_id": "caseloads.income_support",
+   "year": 2025,
+   "value": 24803.04834920168,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "income_support",
+   "label": "Income Support"
+  },
+  {
+   "metric_id": "caseloads.income_support",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "income_support",
+   "label": "Income Support"
+  },
+  {
+   "metric_id": "caseloads.income_support",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "income_support",
+   "label": "Income Support"
+  },
+  {
+   "metric_id": "caseloads.income_support",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "income_support",
+   "label": "Income Support"
+  },
+  {
+   "metric_id": "caseloads.income_support",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "income_support",
+   "label": "Income Support"
+  },
+  {
+   "metric_id": "caseloads.income_support",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "income_support",
+   "label": "Income Support"
+  },
+  {
+   "metric_id": "caseloads.income_support",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "income_support",
+   "label": "Income Support"
+  },
+  {
+   "metric_id": "spending.working_tax_credit",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "working_tax_credit",
+   "label": "Working Tax Credit"
+  },
+  {
+   "metric_id": "spending.working_tax_credit",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "working_tax_credit",
+   "label": "Working Tax Credit"
+  },
+  {
+   "metric_id": "spending.working_tax_credit",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "working_tax_credit",
+   "label": "Working Tax Credit"
+  },
+  {
+   "metric_id": "spending.working_tax_credit",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "working_tax_credit",
+   "label": "Working Tax Credit"
+  },
+  {
+   "metric_id": "spending.working_tax_credit",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "working_tax_credit",
+   "label": "Working Tax Credit"
+  },
+  {
+   "metric_id": "spending.working_tax_credit",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "working_tax_credit",
+   "label": "Working Tax Credit"
+  },
+  {
+   "metric_id": "spending.working_tax_credit",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "working_tax_credit",
+   "label": "Working Tax Credit"
+  },
+  {
+   "metric_id": "caseloads.working_tax_credit",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "working_tax_credit",
+   "label": "Working Tax Credit"
+  },
+  {
+   "metric_id": "caseloads.working_tax_credit",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "working_tax_credit",
+   "label": "Working Tax Credit"
+  },
+  {
+   "metric_id": "caseloads.working_tax_credit",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "working_tax_credit",
+   "label": "Working Tax Credit"
+  },
+  {
+   "metric_id": "caseloads.working_tax_credit",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "working_tax_credit",
+   "label": "Working Tax Credit"
+  },
+  {
+   "metric_id": "caseloads.working_tax_credit",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "working_tax_credit",
+   "label": "Working Tax Credit"
+  },
+  {
+   "metric_id": "caseloads.working_tax_credit",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "working_tax_credit",
+   "label": "Working Tax Credit"
+  },
+  {
+   "metric_id": "caseloads.working_tax_credit",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "working_tax_credit",
+   "label": "Working Tax Credit"
+  },
+  {
+   "metric_id": "spending.child_tax_credit",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "child_tax_credit",
+   "label": "Child Tax Credit"
+  },
+  {
+   "metric_id": "spending.child_tax_credit",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "child_tax_credit",
+   "label": "Child Tax Credit"
+  },
+  {
+   "metric_id": "spending.child_tax_credit",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "child_tax_credit",
+   "label": "Child Tax Credit"
+  },
+  {
+   "metric_id": "spending.child_tax_credit",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "child_tax_credit",
+   "label": "Child Tax Credit"
+  },
+  {
+   "metric_id": "spending.child_tax_credit",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "child_tax_credit",
+   "label": "Child Tax Credit"
+  },
+  {
+   "metric_id": "spending.child_tax_credit",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "child_tax_credit",
+   "label": "Child Tax Credit"
+  },
+  {
+   "metric_id": "spending.child_tax_credit",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "child_tax_credit",
+   "label": "Child Tax Credit"
+  },
+  {
+   "metric_id": "caseloads.child_tax_credit",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "child_tax_credit",
+   "label": "Child Tax Credit"
+  },
+  {
+   "metric_id": "caseloads.child_tax_credit",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "child_tax_credit",
+   "label": "Child Tax Credit"
+  },
+  {
+   "metric_id": "caseloads.child_tax_credit",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "child_tax_credit",
+   "label": "Child Tax Credit"
+  },
+  {
+   "metric_id": "caseloads.child_tax_credit",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "child_tax_credit",
+   "label": "Child Tax Credit"
+  },
+  {
+   "metric_id": "caseloads.child_tax_credit",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "child_tax_credit",
+   "label": "Child Tax Credit"
+  },
+  {
+   "metric_id": "caseloads.child_tax_credit",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "child_tax_credit",
+   "label": "Child Tax Credit"
+  },
+  {
+   "metric_id": "caseloads.child_tax_credit",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "child_tax_credit",
+   "label": "Child Tax Credit"
+  },
+  {
+   "metric_id": "spending.incapacity_benefit",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "incapacity_benefit",
+   "label": "Incapacity Benefit"
+  },
+  {
+   "metric_id": "spending.incapacity_benefit",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "incapacity_benefit",
+   "label": "Incapacity Benefit"
+  },
+  {
+   "metric_id": "spending.incapacity_benefit",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "incapacity_benefit",
+   "label": "Incapacity Benefit"
+  },
+  {
+   "metric_id": "spending.incapacity_benefit",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "incapacity_benefit",
+   "label": "Incapacity Benefit"
+  },
+  {
+   "metric_id": "spending.incapacity_benefit",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "incapacity_benefit",
+   "label": "Incapacity Benefit"
+  },
+  {
+   "metric_id": "spending.incapacity_benefit",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "incapacity_benefit",
+   "label": "Incapacity Benefit"
+  },
+  {
+   "metric_id": "spending.incapacity_benefit",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "incapacity_benefit",
+   "label": "Incapacity Benefit"
+  },
+  {
+   "metric_id": "caseloads.incapacity_benefit",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "incapacity_benefit",
+   "label": "Incapacity Benefit"
+  },
+  {
+   "metric_id": "caseloads.incapacity_benefit",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "incapacity_benefit",
+   "label": "Incapacity Benefit"
+  },
+  {
+   "metric_id": "caseloads.incapacity_benefit",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "incapacity_benefit",
+   "label": "Incapacity Benefit"
+  },
+  {
+   "metric_id": "caseloads.incapacity_benefit",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "incapacity_benefit",
+   "label": "Incapacity Benefit"
+  },
+  {
+   "metric_id": "caseloads.incapacity_benefit",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "incapacity_benefit",
+   "label": "Incapacity Benefit"
+  },
+  {
+   "metric_id": "caseloads.incapacity_benefit",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "incapacity_benefit",
+   "label": "Incapacity Benefit"
+  },
+  {
+   "metric_id": "caseloads.incapacity_benefit",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "incapacity_benefit",
+   "label": "Incapacity Benefit"
+  },
+  {
+   "metric_id": "spending.pip",
+   "year": 2025,
+   "value": 27050796518.958164,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pip",
+   "label": "Personal Independence Payment"
+  },
+  {
+   "metric_id": "spending.pip",
+   "year": 2026,
+   "value": 28187504820.35819,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pip",
+   "label": "Personal Independence Payment"
+  },
+  {
+   "metric_id": "spending.pip",
+   "year": 2027,
+   "value": 28942448438.6212,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pip",
+   "label": "Personal Independence Payment"
+  },
+  {
+   "metric_id": "spending.pip",
+   "year": 2028,
+   "value": 29639334288.533737,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pip",
+   "label": "Personal Independence Payment"
+  },
+  {
+   "metric_id": "spending.pip",
+   "year": 2029,
+   "value": 30365112782.59276,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pip",
+   "label": "Personal Independence Payment"
+  },
+  {
+   "metric_id": "spending.pip",
+   "year": 2030,
+   "value": 31111725337.197277,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pip",
+   "label": "Personal Independence Payment"
+  },
+  {
+   "metric_id": "spending.pip",
+   "year": 2031,
+   "value": 31870426624.368805,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "pip",
+   "label": "Personal Independence Payment"
+  },
+  {
+   "metric_id": "caseloads.pip",
+   "year": 2025,
+   "value": 3581346.50325121,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pip",
+   "label": "Personal Independence Payment"
+  },
+  {
+   "metric_id": "caseloads.pip",
+   "year": 2026,
+   "value": 3594955.7513322085,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pip",
+   "label": "Personal Independence Payment"
+  },
+  {
+   "metric_id": "caseloads.pip",
+   "year": 2027,
+   "value": 3608257.1502955705,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pip",
+   "label": "Personal Independence Payment"
+  },
+  {
+   "metric_id": "caseloads.pip",
+   "year": 2028,
+   "value": 3622690.0020009577,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pip",
+   "label": "Personal Independence Payment"
+  },
+  {
+   "metric_id": "caseloads.pip",
+   "year": 2029,
+   "value": 3638629.8922510594,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pip",
+   "label": "Personal Independence Payment"
+  },
+  {
+   "metric_id": "caseloads.pip",
+   "year": 2030,
+   "value": 3655003.834502399,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pip",
+   "label": "Personal Independence Payment"
+  },
+  {
+   "metric_id": "caseloads.pip",
+   "year": 2031,
+   "value": 3670720.34740828,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "pip",
+   "label": "Personal Independence Payment"
+  },
+  {
+   "metric_id": "spending.dla",
+   "year": 2025,
+   "value": 7519515429.343498,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dla",
+   "label": "Disability Living Allowance"
+  },
+  {
+   "metric_id": "spending.dla",
+   "year": 2026,
+   "value": 7834750092.336622,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dla",
+   "label": "Disability Living Allowance"
+  },
+  {
+   "metric_id": "spending.dla",
+   "year": 2027,
+   "value": 8044587394.8355665,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dla",
+   "label": "Disability Living Allowance"
+  },
+  {
+   "metric_id": "spending.dla",
+   "year": 2028,
+   "value": 8238287553.0089445,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dla",
+   "label": "Disability Living Allowance"
+  },
+  {
+   "metric_id": "spending.dla",
+   "year": 2029,
+   "value": 8440018870.958048,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dla",
+   "label": "Disability Living Allowance"
+  },
+  {
+   "metric_id": "spending.dla",
+   "year": 2030,
+   "value": 8647540725.08345,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dla",
+   "label": "Disability Living Allowance"
+  },
+  {
+   "metric_id": "spending.dla",
+   "year": 2031,
+   "value": 8858422643.224787,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dla",
+   "label": "Disability Living Allowance"
+  },
+  {
+   "metric_id": "caseloads.dla",
+   "year": 2025,
+   "value": 1296293.76486139,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "dla",
+   "label": "Disability Living Allowance"
+  },
+  {
+   "metric_id": "caseloads.dla",
+   "year": 2026,
+   "value": 1301219.7249682397,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "dla",
+   "label": "Disability Living Allowance"
+  },
+  {
+   "metric_id": "caseloads.dla",
+   "year": 2027,
+   "value": 1306034.2623415291,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "dla",
+   "label": "Disability Living Allowance"
+  },
+  {
+   "metric_id": "caseloads.dla",
+   "year": 2028,
+   "value": 1311258.3262985945,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "dla",
+   "label": "Disability Living Allowance"
+  },
+  {
+   "metric_id": "caseloads.dla",
+   "year": 2029,
+   "value": 1317027.8841654807,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "dla",
+   "label": "Disability Living Allowance"
+  },
+  {
+   "metric_id": "caseloads.dla",
+   "year": 2030,
+   "value": 1322954.550306335,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "dla",
+   "label": "Disability Living Allowance"
+  },
+  {
+   "metric_id": "caseloads.dla",
+   "year": 2031,
+   "value": 1328643.2488841116,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "dla",
+   "label": "Disability Living Allowance"
+  },
+  {
+   "metric_id": "spending.attendance_allowance",
+   "year": 2025,
+   "value": 8713069613.537264,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "attendance_allowance",
+   "label": "Attendance Allowance"
+  },
+  {
+   "metric_id": "spending.attendance_allowance",
+   "year": 2026,
+   "value": 9078584120.844625,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "attendance_allowance",
+   "label": "Attendance Allowance"
+  },
+  {
+   "metric_id": "spending.attendance_allowance",
+   "year": 2027,
+   "value": 9321734807.6616,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "attendance_allowance",
+   "label": "Attendance Allowance"
+  },
+  {
+   "metric_id": "spending.attendance_allowance",
+   "year": 2028,
+   "value": 9546186520.166021,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "attendance_allowance",
+   "label": "Attendance Allowance"
+  },
+  {
+   "metric_id": "spending.attendance_allowance",
+   "year": 2029,
+   "value": 9779944679.35845,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "attendance_allowance",
+   "label": "Attendance Allowance"
+  },
+  {
+   "metric_id": "spending.attendance_allowance",
+   "year": 2030,
+   "value": 10020412141.96302,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "attendance_allowance",
+   "label": "Attendance Allowance"
+  },
+  {
+   "metric_id": "spending.attendance_allowance",
+   "year": 2031,
+   "value": 10264773293.632841,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "attendance_allowance",
+   "label": "Attendance Allowance"
+  },
+  {
+   "metric_id": "caseloads.attendance_allowance",
+   "year": 2025,
+   "value": 1701405.523672521,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "attendance_allowance",
+   "label": "Attendance Allowance"
+  },
+  {
+   "metric_id": "caseloads.attendance_allowance",
+   "year": 2026,
+   "value": 1707870.9254432172,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "attendance_allowance",
+   "label": "Attendance Allowance"
+  },
+  {
+   "metric_id": "caseloads.attendance_allowance",
+   "year": 2027,
+   "value": 1714190.0767204762,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "attendance_allowance",
+   "label": "Attendance Allowance"
+  },
+  {
+   "metric_id": "caseloads.attendance_allowance",
+   "year": 2028,
+   "value": 1721046.7521996498,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "attendance_allowance",
+   "label": "Attendance Allowance"
+  },
+  {
+   "metric_id": "caseloads.attendance_allowance",
+   "year": 2029,
+   "value": 1728619.3880579919,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "attendance_allowance",
+   "label": "Attendance Allowance"
+  },
+  {
+   "metric_id": "caseloads.attendance_allowance",
+   "year": 2030,
+   "value": 1736398.2265275419,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "attendance_allowance",
+   "label": "Attendance Allowance"
+  },
+  {
+   "metric_id": "caseloads.attendance_allowance",
+   "year": 2031,
+   "value": 1743864.7361019552,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "attendance_allowance",
+   "label": "Attendance Allowance"
+  },
+  {
+   "metric_id": "spending.carers_allowance",
+   "year": 2025,
+   "value": 4498304384.942733,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "carers_allowance",
+   "label": "Carer's Allowance"
+  },
+  {
+   "metric_id": "spending.carers_allowance",
+   "year": 2026,
+   "value": 4686148233.621847,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "carers_allowance",
+   "label": "Carer's Allowance"
+  },
+  {
+   "metric_id": "spending.carers_allowance",
+   "year": 2027,
+   "value": 4811656899.117326,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "carers_allowance",
+   "label": "Carer's Allowance"
+  },
+  {
+   "metric_id": "spending.carers_allowance",
+   "year": 2028,
+   "value": 4927513579.343729,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "carers_allowance",
+   "label": "Carer's Allowance"
+  },
+  {
+   "metric_id": "spending.carers_allowance",
+   "year": 2029,
+   "value": 5048174373.178411,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "carers_allowance",
+   "label": "Carer's Allowance"
+  },
+  {
+   "metric_id": "spending.carers_allowance",
+   "year": 2030,
+   "value": 5168683430.088493,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "carers_allowance",
+   "label": "Carer's Allowance"
+  },
+  {
+   "metric_id": "spending.carers_allowance",
+   "year": 2031,
+   "value": 5294728594.298446,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "carers_allowance",
+   "label": "Carer's Allowance"
+  },
+  {
+   "metric_id": "caseloads.carers_allowance",
+   "year": 2025,
+   "value": 1038485.6135211289,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "carers_allowance",
+   "label": "Carer's Allowance"
+  },
+  {
+   "metric_id": "caseloads.carers_allowance",
+   "year": 2026,
+   "value": 1042431.8938074112,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "carers_allowance",
+   "label": "Carer's Allowance"
+  },
+  {
+   "metric_id": "caseloads.carers_allowance",
+   "year": 2027,
+   "value": 1046288.9127022028,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "carers_allowance",
+   "label": "Carer's Allowance"
+  },
+  {
+   "metric_id": "caseloads.carers_allowance",
+   "year": 2028,
+   "value": 1050474.0211501122,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "carers_allowance",
+   "label": "Carer's Allowance"
+  },
+  {
+   "metric_id": "caseloads.carers_allowance",
+   "year": 2029,
+   "value": 1055096.1227512658,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "carers_allowance",
+   "label": "Carer's Allowance"
+  },
+  {
+   "metric_id": "caseloads.carers_allowance",
+   "year": 2030,
+   "value": 1059103.4548994005,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "carers_allowance",
+   "label": "Carer's Allowance"
+  },
+  {
+   "metric_id": "caseloads.carers_allowance",
+   "year": 2031,
+   "value": 1063657.5973686576,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "carers_allowance",
+   "label": "Carer's Allowance"
+  },
+  {
+   "metric_id": "spending.carer_support_payment",
+   "year": 2025,
+   "value": 352284662.9714799,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "carer_support_payment",
+   "label": "Carer Support Payment (Scotland)"
+  },
+  {
+   "metric_id": "spending.carer_support_payment",
+   "year": 2026,
+   "value": 366995656.0579343,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "carer_support_payment",
+   "label": "Carer Support Payment (Scotland)"
+  },
+  {
+   "metric_id": "spending.carer_support_payment",
+   "year": 2027,
+   "value": 376824870.322693,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "carer_support_payment",
+   "label": "Carer Support Payment (Scotland)"
+  },
+  {
+   "metric_id": "spending.carer_support_payment",
+   "year": 2028,
+   "value": 385898182.16357577,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "carer_support_payment",
+   "label": "Carer Support Payment (Scotland)"
+  },
+  {
+   "metric_id": "spending.carer_support_payment",
+   "year": 2029,
+   "value": 395347728.7181926,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "carer_support_payment",
+   "label": "Carer Support Payment (Scotland)"
+  },
+  {
+   "metric_id": "spending.carer_support_payment",
+   "year": 2030,
+   "value": 405068460.50986433,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "carer_support_payment",
+   "label": "Carer Support Payment (Scotland)"
+  },
+  {
+   "metric_id": "spending.carer_support_payment",
+   "year": 2031,
+   "value": 414946587.8089589,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "carer_support_payment",
+   "label": "Carer Support Payment (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.carer_support_payment",
+   "year": 2025,
+   "value": 81328.99044907093,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "carer_support_payment",
+   "label": "Carer Support Payment (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.carer_support_payment",
+   "year": 2026,
+   "value": 81638.04422974586,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "carer_support_payment",
+   "label": "Carer Support Payment (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.carer_support_payment",
+   "year": 2027,
+   "value": 81940.10755866766,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "carer_support_payment",
+   "label": "Carer Support Payment (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.carer_support_payment",
+   "year": 2028,
+   "value": 82267.86362826824,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "carer_support_payment",
+   "label": "Carer Support Payment (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.carer_support_payment",
+   "year": 2029,
+   "value": 82629.84296369553,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "carer_support_payment",
+   "label": "Carer Support Payment (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.carer_support_payment",
+   "year": 2030,
+   "value": 83001.67959588766,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "carer_support_payment",
+   "label": "Carer Support Payment (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.carer_support_payment",
+   "year": 2031,
+   "value": 83358.58633065224,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "carer_support_payment",
+   "label": "Carer Support Payment (Scotland)"
+  },
+  {
+   "metric_id": "spending.scottish_carer_supplement",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "scottish_carer_supplement",
+   "label": "Carer's Allowance Supplement (Scotland)"
+  },
+  {
+   "metric_id": "spending.scottish_carer_supplement",
+   "year": 2026,
+   "value": 49668588.102493696,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "scottish_carer_supplement",
+   "label": "Carer's Allowance Supplement (Scotland)"
+  },
+  {
+   "metric_id": "spending.scottish_carer_supplement",
+   "year": 2027,
+   "value": 49852363.43918431,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "scottish_carer_supplement",
+   "label": "Carer's Allowance Supplement (Scotland)"
+  },
+  {
+   "metric_id": "spending.scottish_carer_supplement",
+   "year": 2028,
+   "value": 50051770.239931166,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "scottish_carer_supplement",
+   "label": "Carer's Allowance Supplement (Scotland)"
+  },
+  {
+   "metric_id": "spending.scottish_carer_supplement",
+   "year": 2029,
+   "value": 50271998.47644251,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "scottish_carer_supplement",
+   "label": "Carer's Allowance Supplement (Scotland)"
+  },
+  {
+   "metric_id": "spending.scottish_carer_supplement",
+   "year": 2030,
+   "value": 50498223.892546244,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "scottish_carer_supplement",
+   "label": "Carer's Allowance Supplement (Scotland)"
+  },
+  {
+   "metric_id": "spending.scottish_carer_supplement",
+   "year": 2031,
+   "value": 50715365.95869056,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "scottish_carer_supplement",
+   "label": "Carer's Allowance Supplement (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.scottish_carer_supplement",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "scottish_carer_supplement",
+   "label": "Carer's Allowance Supplement (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.scottish_carer_supplement",
+   "year": 2026,
+   "value": 81638.04422974586,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "scottish_carer_supplement",
+   "label": "Carer's Allowance Supplement (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.scottish_carer_supplement",
+   "year": 2027,
+   "value": 81940.10755866766,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "scottish_carer_supplement",
+   "label": "Carer's Allowance Supplement (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.scottish_carer_supplement",
+   "year": 2028,
+   "value": 82267.86362826824,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "scottish_carer_supplement",
+   "label": "Carer's Allowance Supplement (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.scottish_carer_supplement",
+   "year": 2029,
+   "value": 82629.84296369553,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "scottish_carer_supplement",
+   "label": "Carer's Allowance Supplement (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.scottish_carer_supplement",
+   "year": 2030,
+   "value": 83001.67959588766,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "scottish_carer_supplement",
+   "label": "Carer's Allowance Supplement (Scotland)"
+  },
+  {
+   "metric_id": "caseloads.scottish_carer_supplement",
+   "year": 2031,
+   "value": 83358.58633065224,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "scottish_carer_supplement",
+   "label": "Carer's Allowance Supplement (Scotland)"
+  },
+  {
+   "metric_id": "spending.sda",
+   "year": 2025,
+   "value": 26001911.186065674,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "sda",
+   "label": "Severe Disablement Allowance"
+  },
+  {
+   "metric_id": "spending.sda",
+   "year": 2026,
+   "value": 27088007.985394344,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "sda",
+   "label": "Severe Disablement Allowance"
+  },
+  {
+   "metric_id": "spending.sda",
+   "year": 2027,
+   "value": 27813501.58527428,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "sda",
+   "label": "Severe Disablement Allowance"
+  },
+  {
+   "metric_id": "spending.sda",
+   "year": 2028,
+   "value": 28483206.305987563,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "sda",
+   "label": "Severe Disablement Allowance"
+  },
+  {
+   "metric_id": "spending.sda",
+   "year": 2029,
+   "value": 29180676.754239403,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "sda",
+   "label": "Severe Disablement Allowance"
+  },
+  {
+   "metric_id": "spending.sda",
+   "year": 2030,
+   "value": 29898167.047436927,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "sda",
+   "label": "Severe Disablement Allowance"
+  },
+  {
+   "metric_id": "spending.sda",
+   "year": 2031,
+   "value": 30627272.264840692,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "sda",
+   "label": "Severe Disablement Allowance"
+  },
+  {
+   "metric_id": "caseloads.sda",
+   "year": 2025,
+   "value": 4348.145683288574,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "sda",
+   "label": "Severe Disablement Allowance"
+  },
+  {
+   "metric_id": "caseloads.sda",
+   "year": 2026,
+   "value": 4364.668739795685,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "sda",
+   "label": "Severe Disablement Allowance"
+  },
+  {
+   "metric_id": "caseloads.sda",
+   "year": 2027,
+   "value": 4380.818064212799,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "sda",
+   "label": "Severe Disablement Allowance"
+  },
+  {
+   "metric_id": "caseloads.sda",
+   "year": 2028,
+   "value": 4398.341153144836,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "sda",
+   "label": "Severe Disablement Allowance"
+  },
+  {
+   "metric_id": "caseloads.sda",
+   "year": 2029,
+   "value": 4417.694038391113,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "sda",
+   "label": "Severe Disablement Allowance"
+  },
+  {
+   "metric_id": "caseloads.sda",
+   "year": 2030,
+   "value": 4437.573884487152,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "sda",
+   "label": "Severe Disablement Allowance"
+  },
+  {
+   "metric_id": "caseloads.sda",
+   "year": 2031,
+   "value": 4456.655462741852,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "sda",
+   "label": "Severe Disablement Allowance"
+  },
+  {
+   "metric_id": "spending.iidb",
+   "year": 2025,
+   "value": 505396316.7142715,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "iidb",
+   "label": "Industrial Injuries Disablement Benefit"
+  },
+  {
+   "metric_id": "spending.iidb",
+   "year": 2026,
+   "value": 518985130.7359581,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "iidb",
+   "label": "Industrial Injuries Disablement Benefit"
+  },
+  {
+   "metric_id": "spending.iidb",
+   "year": 2027,
+   "value": 531323482.42515385,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "iidb",
+   "label": "Industrial Injuries Disablement Benefit"
+  },
+  {
+   "metric_id": "spending.iidb",
+   "year": 2028,
+   "value": 544117734.2383146,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "iidb",
+   "label": "Industrial Injuries Disablement Benefit"
+  },
+  {
+   "metric_id": "spending.iidb",
+   "year": 2029,
+   "value": 557442066.9128101,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "iidb",
+   "label": "Industrial Injuries Disablement Benefit"
+  },
+  {
+   "metric_id": "spending.iidb",
+   "year": 2030,
+   "value": 571149605.5284191,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "iidb",
+   "label": "Industrial Injuries Disablement Benefit"
+  },
+  {
+   "metric_id": "spending.iidb",
+   "year": 2031,
+   "value": 585077668.0455391,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "iidb",
+   "label": "Industrial Injuries Disablement Benefit"
+  },
+  {
+   "metric_id": "caseloads.iidb",
+   "year": 2025,
+   "value": 137473.5583550036,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "iidb",
+   "label": "Industrial Injuries Disablement Benefit"
+  },
+  {
+   "metric_id": "caseloads.iidb",
+   "year": 2026,
+   "value": 137995.96156597137,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "iidb",
+   "label": "Industrial Injuries Disablement Benefit"
+  },
+  {
+   "metric_id": "caseloads.iidb",
+   "year": 2027,
+   "value": 138506.54905071855,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "iidb",
+   "label": "Industrial Injuries Disablement Benefit"
+  },
+  {
+   "metric_id": "caseloads.iidb",
+   "year": 2028,
+   "value": 139060.56764179468,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "iidb",
+   "label": "Industrial Injuries Disablement Benefit"
+  },
+  {
+   "metric_id": "caseloads.iidb",
+   "year": 2029,
+   "value": 139672.4357754588,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "iidb",
+   "label": "Industrial Injuries Disablement Benefit"
+  },
+  {
+   "metric_id": "caseloads.iidb",
+   "year": 2030,
+   "value": 140300.96557214856,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "iidb",
+   "label": "Industrial Injuries Disablement Benefit"
+  },
+  {
+   "metric_id": "caseloads.iidb",
+   "year": 2031,
+   "value": 140904.25975701213,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "iidb",
+   "label": "Industrial Injuries Disablement Benefit"
+  },
+  {
+   "metric_id": "spending.afcs",
+   "year": 2025,
+   "value": 608337715.55495,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "afcs",
+   "label": "Armed Forces Compensation Scheme"
+  },
+  {
+   "metric_id": "spending.afcs",
+   "year": 2026,
+   "value": 624694356.9666108,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "afcs",
+   "label": "Armed Forces Compensation Scheme"
+  },
+  {
+   "metric_id": "spending.afcs",
+   "year": 2027,
+   "value": 639545878.8404102,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "afcs",
+   "label": "Armed Forces Compensation Scheme"
+  },
+  {
+   "metric_id": "spending.afcs",
+   "year": 2028,
+   "value": 654946115.3175836,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "afcs",
+   "label": "Armed Forces Compensation Scheme"
+  },
+  {
+   "metric_id": "spending.afcs",
+   "year": 2029,
+   "value": 670984438.3106201,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "afcs",
+   "label": "Armed Forces Compensation Scheme"
+  },
+  {
+   "metric_id": "spending.afcs",
+   "year": 2030,
+   "value": 687483985.4915761,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "afcs",
+   "label": "Armed Forces Compensation Scheme"
+  },
+  {
+   "metric_id": "spending.afcs",
+   "year": 2031,
+   "value": 704248973.1763016,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "afcs",
+   "label": "Armed Forces Compensation Scheme"
+  },
+  {
+   "metric_id": "caseloads.afcs",
+   "year": 2025,
+   "value": 93556.20288914442,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "afcs",
+   "label": "Armed Forces Compensation Scheme"
+  },
+  {
+   "metric_id": "caseloads.afcs",
+   "year": 2026,
+   "value": 93911.72057116032,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "afcs",
+   "label": "Armed Forces Compensation Scheme"
+  },
+  {
+   "metric_id": "caseloads.afcs",
+   "year": 2027,
+   "value": 94259.19729825854,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "afcs",
+   "label": "Armed Forces Compensation Scheme"
+  },
+  {
+   "metric_id": "caseloads.afcs",
+   "year": 2028,
+   "value": 94636.23070755601,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "afcs",
+   "label": "Armed Forces Compensation Scheme"
+  },
+  {
+   "metric_id": "caseloads.afcs",
+   "year": 2029,
+   "value": 95052.63239446282,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "afcs",
+   "label": "Armed Forces Compensation Scheme"
+  },
+  {
+   "metric_id": "caseloads.afcs",
+   "year": 2030,
+   "value": 95480.37335321307,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "afcs",
+   "label": "Armed Forces Compensation Scheme"
+  },
+  {
+   "metric_id": "caseloads.afcs",
+   "year": 2031,
+   "value": 95890.9378361702,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "afcs",
+   "label": "Armed Forces Compensation Scheme"
+  },
+  {
+   "metric_id": "spending.child_benefit",
+   "year": 2025,
+   "value": 16193470845.603186,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "child_benefit",
+   "label": "Child Benefit"
+  },
+  {
+   "metric_id": "spending.child_benefit",
+   "year": 2026,
+   "value": 16865082606.210407,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "child_benefit",
+   "label": "Child Benefit"
+  },
+  {
+   "metric_id": "spending.child_benefit",
+   "year": 2027,
+   "value": 17309224587.32687,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "child_benefit",
+   "label": "Child Benefit"
+  },
+  {
+   "metric_id": "spending.child_benefit",
+   "year": 2028,
+   "value": 17722732649.511974,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "child_benefit",
+   "label": "Child Benefit"
+  },
+  {
+   "metric_id": "spending.child_benefit",
+   "year": 2029,
+   "value": 18149355497.8589,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "child_benefit",
+   "label": "Child Benefit"
+  },
+  {
+   "metric_id": "spending.child_benefit",
+   "year": 2030,
+   "value": 18589683648.812527,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "child_benefit",
+   "label": "Child Benefit"
+  },
+  {
+   "metric_id": "spending.child_benefit",
+   "year": 2031,
+   "value": 19019347515.30189,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "child_benefit",
+   "label": "Child Benefit"
+  },
+  {
+   "metric_id": "caseloads.child_benefit",
+   "year": 2025,
+   "value": 8124933.04077065,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "child_benefit",
+   "label": "Child Benefit"
+  },
+  {
+   "metric_id": "caseloads.child_benefit",
+   "year": 2026,
+   "value": 8151661.30214186,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "child_benefit",
+   "label": "Child Benefit"
+  },
+  {
+   "metric_id": "caseloads.child_benefit",
+   "year": 2027,
+   "value": 8178496.28433007,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "child_benefit",
+   "label": "Child Benefit"
+  },
+  {
+   "metric_id": "caseloads.child_benefit",
+   "year": 2028,
+   "value": 8209901.52580525,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "child_benefit",
+   "label": "Child Benefit"
+  },
+  {
+   "metric_id": "caseloads.child_benefit",
+   "year": 2029,
+   "value": 8242994.678169802,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "child_benefit",
+   "label": "Child Benefit"
+  },
+  {
+   "metric_id": "caseloads.child_benefit",
+   "year": 2030,
+   "value": 8277445.224295765,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "child_benefit",
+   "label": "Child Benefit"
+  },
+  {
+   "metric_id": "caseloads.child_benefit",
+   "year": 2031,
+   "value": 8301138.966311127,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "child_benefit",
+   "label": "Child Benefit"
+  },
+  {
+   "metric_id": "spending.scottish_child_payment",
+   "year": 2025,
+   "value": 496826543.81597674,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "scottish_child_payment",
+   "label": "Scottish Child Payment"
+  },
+  {
+   "metric_id": "spending.scottish_child_payment",
+   "year": 2026,
+   "value": 518442587.758203,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "scottish_child_payment",
+   "label": "Scottish Child Payment"
+  },
+  {
+   "metric_id": "spending.scottish_child_payment",
+   "year": 2027,
+   "value": 532354940.6639104,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "scottish_child_payment",
+   "label": "Scottish Child Payment"
+  },
+  {
+   "metric_id": "spending.scottish_child_payment",
+   "year": 2028,
+   "value": 545600150.271133,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "scottish_child_payment",
+   "label": "Scottish Child Payment"
+  },
+  {
+   "metric_id": "spending.scottish_child_payment",
+   "year": 2029,
+   "value": 557257209.815226,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "scottish_child_payment",
+   "label": "Scottish Child Payment"
+  },
+  {
+   "metric_id": "spending.scottish_child_payment",
+   "year": 2030,
+   "value": 570941579.8039502,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "scottish_child_payment",
+   "label": "Scottish Child Payment"
+  },
+  {
+   "metric_id": "spending.scottish_child_payment",
+   "year": 2031,
+   "value": 579837131.7746521,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "scottish_child_payment",
+   "label": "Scottish Child Payment"
+  },
+  {
+   "metric_id": "caseloads.scottish_child_payment",
+   "year": 2025,
+   "value": 351909.9919484854,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "scottish_child_payment",
+   "label": "Scottish Child Payment"
+  },
+  {
+   "metric_id": "caseloads.scottish_child_payment",
+   "year": 2026,
+   "value": 353547.8581060171,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "scottish_child_payment",
+   "label": "Scottish Child Payment"
+  },
+  {
+   "metric_id": "caseloads.scottish_child_payment",
+   "year": 2027,
+   "value": 354855.99119508266,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "scottish_child_payment",
+   "label": "Scottish Child Payment"
+  },
+  {
+   "metric_id": "caseloads.scottish_child_payment",
+   "year": 2028,
+   "value": 356275.3960905075,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "scottish_child_payment",
+   "label": "Scottish Child Payment"
+  },
+  {
+   "metric_id": "caseloads.scottish_child_payment",
+   "year": 2029,
+   "value": 356621.7960590124,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "scottish_child_payment",
+   "label": "Scottish Child Payment"
+  },
+  {
+   "metric_id": "caseloads.scottish_child_payment",
+   "year": 2030,
+   "value": 358226.60453784466,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "scottish_child_payment",
+   "label": "Scottish Child Payment"
+  },
+  {
+   "metric_id": "caseloads.scottish_child_payment",
+   "year": 2031,
+   "value": 356674.22713041306,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "scottish_child_payment",
+   "label": "Scottish Child Payment"
+  },
+  {
+   "metric_id": "spending.two_child_limit_payment",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "two_child_limit_payment",
+   "label": "Two Child Limit Payment (Scotland, counterfactual)"
+  },
+  {
+   "metric_id": "spending.two_child_limit_payment",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "two_child_limit_payment",
+   "label": "Two Child Limit Payment (Scotland, counterfactual)"
+  },
+  {
+   "metric_id": "spending.two_child_limit_payment",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "two_child_limit_payment",
+   "label": "Two Child Limit Payment (Scotland, counterfactual)"
+  },
+  {
+   "metric_id": "spending.two_child_limit_payment",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "two_child_limit_payment",
+   "label": "Two Child Limit Payment (Scotland, counterfactual)"
+  },
+  {
+   "metric_id": "spending.two_child_limit_payment",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "two_child_limit_payment",
+   "label": "Two Child Limit Payment (Scotland, counterfactual)"
+  },
+  {
+   "metric_id": "spending.two_child_limit_payment",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "two_child_limit_payment",
+   "label": "Two Child Limit Payment (Scotland, counterfactual)"
+  },
+  {
+   "metric_id": "spending.two_child_limit_payment",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "two_child_limit_payment",
+   "label": "Two Child Limit Payment (Scotland, counterfactual)"
+  },
+  {
+   "metric_id": "caseloads.two_child_limit_payment",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "two_child_limit_payment",
+   "label": "Two Child Limit Payment (Scotland, counterfactual)"
+  },
+  {
+   "metric_id": "caseloads.two_child_limit_payment",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "two_child_limit_payment",
+   "label": "Two Child Limit Payment (Scotland, counterfactual)"
+  },
+  {
+   "metric_id": "caseloads.two_child_limit_payment",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "two_child_limit_payment",
+   "label": "Two Child Limit Payment (Scotland, counterfactual)"
+  },
+  {
+   "metric_id": "caseloads.two_child_limit_payment",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "two_child_limit_payment",
+   "label": "Two Child Limit Payment (Scotland, counterfactual)"
+  },
+  {
+   "metric_id": "caseloads.two_child_limit_payment",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "two_child_limit_payment",
+   "label": "Two Child Limit Payment (Scotland, counterfactual)"
+  },
+  {
+   "metric_id": "caseloads.two_child_limit_payment",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "two_child_limit_payment",
+   "label": "Two Child Limit Payment (Scotland, counterfactual)"
+  },
+  {
+   "metric_id": "caseloads.two_child_limit_payment",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "two_child_limit_payment",
+   "label": "Two Child Limit Payment (Scotland, counterfactual)"
+  },
+  {
+   "metric_id": "spending.healthy_start_vouchers",
+   "year": 2025,
+   "value": 52030589.486631,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "healthy_start_vouchers",
+   "label": "Healthy Start vouchers"
+  },
+  {
+   "metric_id": "spending.healthy_start_vouchers",
+   "year": 2026,
+   "value": 52228307.60199582,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "healthy_start_vouchers",
+   "label": "Healthy Start vouchers"
+  },
+  {
+   "metric_id": "spending.healthy_start_vouchers",
+   "year": 2027,
+   "value": 52421553.321768366,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "healthy_start_vouchers",
+   "label": "Healthy Start vouchers"
+  },
+  {
+   "metric_id": "spending.healthy_start_vouchers",
+   "year": 2028,
+   "value": 52631236.984368056,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "healthy_start_vouchers",
+   "label": "Healthy Start vouchers"
+  },
+  {
+   "metric_id": "spending.healthy_start_vouchers",
+   "year": 2029,
+   "value": 52862815.027035154,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "healthy_start_vouchers",
+   "label": "Healthy Start vouchers"
+  },
+  {
+   "metric_id": "spending.healthy_start_vouchers",
+   "year": 2030,
+   "value": 53100698.99261543,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "healthy_start_vouchers",
+   "label": "Healthy Start vouchers"
+  },
+  {
+   "metric_id": "spending.healthy_start_vouchers",
+   "year": 2031,
+   "value": 53329031.82253713,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "healthy_start_vouchers",
+   "label": "Healthy Start vouchers"
+  },
+  {
+   "metric_id": "caseloads.healthy_start_vouchers",
+   "year": 2025,
+   "value": 200297.54610317945,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "healthy_start_vouchers",
+   "label": "Healthy Start vouchers"
+  },
+  {
+   "metric_id": "caseloads.healthy_start_vouchers",
+   "year": 2026,
+   "value": 201058.68415096402,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "healthy_start_vouchers",
+   "label": "Healthy Start vouchers"
+  },
+  {
+   "metric_id": "caseloads.healthy_start_vouchers",
+   "year": 2027,
+   "value": 201802.60497659445,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "healthy_start_vouchers",
+   "label": "Healthy Start vouchers"
+  },
+  {
+   "metric_id": "caseloads.healthy_start_vouchers",
+   "year": 2028,
+   "value": 202609.80542340875,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "healthy_start_vouchers",
+   "label": "Healthy Start vouchers"
+  },
+  {
+   "metric_id": "caseloads.healthy_start_vouchers",
+   "year": 2029,
+   "value": 203501.29058858752,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "healthy_start_vouchers",
+   "label": "Healthy Start vouchers"
+  },
+  {
+   "metric_id": "caseloads.healthy_start_vouchers",
+   "year": 2030,
+   "value": 204417.0512445569,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "healthy_start_vouchers",
+   "label": "Healthy Start vouchers"
+  },
+  {
+   "metric_id": "caseloads.healthy_start_vouchers",
+   "year": 2031,
+   "value": 205296.04370743036,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "healthy_start_vouchers",
+   "label": "Healthy Start vouchers"
+  },
+  {
+   "metric_id": "spending.maternity_allowance",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "maternity_allowance",
+   "label": "Maternity Allowance"
+  },
+  {
+   "metric_id": "spending.maternity_allowance",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "maternity_allowance",
+   "label": "Maternity Allowance"
+  },
+  {
+   "metric_id": "spending.maternity_allowance",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "maternity_allowance",
+   "label": "Maternity Allowance"
+  },
+  {
+   "metric_id": "spending.maternity_allowance",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "maternity_allowance",
+   "label": "Maternity Allowance"
+  },
+  {
+   "metric_id": "spending.maternity_allowance",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "maternity_allowance",
+   "label": "Maternity Allowance"
+  },
+  {
+   "metric_id": "spending.maternity_allowance",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "maternity_allowance",
+   "label": "Maternity Allowance"
+  },
+  {
+   "metric_id": "spending.maternity_allowance",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "maternity_allowance",
+   "label": "Maternity Allowance"
+  },
+  {
+   "metric_id": "caseloads.maternity_allowance",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "maternity_allowance",
+   "label": "Maternity Allowance"
+  },
+  {
+   "metric_id": "caseloads.maternity_allowance",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "maternity_allowance",
+   "label": "Maternity Allowance"
+  },
+  {
+   "metric_id": "caseloads.maternity_allowance",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "maternity_allowance",
+   "label": "Maternity Allowance"
+  },
+  {
+   "metric_id": "caseloads.maternity_allowance",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "maternity_allowance",
+   "label": "Maternity Allowance"
+  },
+  {
+   "metric_id": "caseloads.maternity_allowance",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "maternity_allowance",
+   "label": "Maternity Allowance"
+  },
+  {
+   "metric_id": "caseloads.maternity_allowance",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "maternity_allowance",
+   "label": "Maternity Allowance"
+  },
+  {
+   "metric_id": "caseloads.maternity_allowance",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "maternity_allowance",
+   "label": "Maternity Allowance"
+  },
+  {
+   "metric_id": "spending.statutory_maternity_pay",
+   "year": 2025,
+   "value": 3318182317.450847,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_maternity_pay",
+   "label": "Statutory Maternity Pay"
+  },
+  {
+   "metric_id": "spending.statutory_maternity_pay",
+   "year": 2026,
+   "value": 3407399609.1208797,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_maternity_pay",
+   "label": "Statutory Maternity Pay"
+  },
+  {
+   "metric_id": "spending.statutory_maternity_pay",
+   "year": 2027,
+   "value": 3488407179.3860164,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_maternity_pay",
+   "label": "Statutory Maternity Pay"
+  },
+  {
+   "metric_id": "spending.statutory_maternity_pay",
+   "year": 2028,
+   "value": 3572407800.478435,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_maternity_pay",
+   "label": "Statutory Maternity Pay"
+  },
+  {
+   "metric_id": "spending.statutory_maternity_pay",
+   "year": 2029,
+   "value": 3659888978.871894,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_maternity_pay",
+   "label": "Statutory Maternity Pay"
+  },
+  {
+   "metric_id": "spending.statutory_maternity_pay",
+   "year": 2030,
+   "value": 3749885904.2724175,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_maternity_pay",
+   "label": "Statutory Maternity Pay"
+  },
+  {
+   "metric_id": "spending.statutory_maternity_pay",
+   "year": 2031,
+   "value": 3841330547.982543,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_maternity_pay",
+   "label": "Statutory Maternity Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_maternity_pay",
+   "year": 2025,
+   "value": 333845.906548053,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_maternity_pay",
+   "label": "Statutory Maternity Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_maternity_pay",
+   "year": 2026,
+   "value": 335114.53039878607,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_maternity_pay",
+   "label": "Statutory Maternity Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_maternity_pay",
+   "year": 2027,
+   "value": 336354.45804458857,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_maternity_pay",
+   "label": "Statutory Maternity Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_maternity_pay",
+   "year": 2028,
+   "value": 337699.85739299655,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_maternity_pay",
+   "label": "Statutory Maternity Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_maternity_pay",
+   "year": 2029,
+   "value": 339185.7411582172,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_maternity_pay",
+   "label": "Statutory Maternity Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_maternity_pay",
+   "year": 2030,
+   "value": 340712.0907050073,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_maternity_pay",
+   "label": "Statutory Maternity Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_maternity_pay",
+   "year": 2031,
+   "value": 342177.1517958939,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_maternity_pay",
+   "label": "Statutory Maternity Pay"
+  },
+  {
+   "metric_id": "spending.statutory_paternity_pay",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_paternity_pay",
+   "label": "Statutory Paternity Pay"
+  },
+  {
+   "metric_id": "spending.statutory_paternity_pay",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_paternity_pay",
+   "label": "Statutory Paternity Pay"
+  },
+  {
+   "metric_id": "spending.statutory_paternity_pay",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_paternity_pay",
+   "label": "Statutory Paternity Pay"
+  },
+  {
+   "metric_id": "spending.statutory_paternity_pay",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_paternity_pay",
+   "label": "Statutory Paternity Pay"
+  },
+  {
+   "metric_id": "spending.statutory_paternity_pay",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_paternity_pay",
+   "label": "Statutory Paternity Pay"
+  },
+  {
+   "metric_id": "spending.statutory_paternity_pay",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_paternity_pay",
+   "label": "Statutory Paternity Pay"
+  },
+  {
+   "metric_id": "spending.statutory_paternity_pay",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_paternity_pay",
+   "label": "Statutory Paternity Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_paternity_pay",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_paternity_pay",
+   "label": "Statutory Paternity Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_paternity_pay",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_paternity_pay",
+   "label": "Statutory Paternity Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_paternity_pay",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_paternity_pay",
+   "label": "Statutory Paternity Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_paternity_pay",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_paternity_pay",
+   "label": "Statutory Paternity Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_paternity_pay",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_paternity_pay",
+   "label": "Statutory Paternity Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_paternity_pay",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_paternity_pay",
+   "label": "Statutory Paternity Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_paternity_pay",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_paternity_pay",
+   "label": "Statutory Paternity Pay"
+  },
+  {
+   "metric_id": "spending.statutory_sick_pay",
+   "year": 2025,
+   "value": 1129666049.2058818,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_sick_pay",
+   "label": "Statutory Sick Pay"
+  },
+  {
+   "metric_id": "spending.statutory_sick_pay",
+   "year": 2026,
+   "value": 1160039811.2081735,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_sick_pay",
+   "label": "Statutory Sick Pay"
+  },
+  {
+   "metric_id": "spending.statutory_sick_pay",
+   "year": 2027,
+   "value": 1187618628.8419445,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_sick_pay",
+   "label": "Statutory Sick Pay"
+  },
+  {
+   "metric_id": "spending.statutory_sick_pay",
+   "year": 2028,
+   "value": 1216216439.2552707,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_sick_pay",
+   "label": "Statutory Sick Pay"
+  },
+  {
+   "metric_id": "spending.statutory_sick_pay",
+   "year": 2029,
+   "value": 1245999166.4642668,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_sick_pay",
+   "label": "Statutory Sick Pay"
+  },
+  {
+   "metric_id": "spending.statutory_sick_pay",
+   "year": 2030,
+   "value": 1276638374.6988342,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_sick_pay",
+   "label": "Statutory Sick Pay"
+  },
+  {
+   "metric_id": "spending.statutory_sick_pay",
+   "year": 2031,
+   "value": 1307770469.3172994,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "statutory_sick_pay",
+   "label": "Statutory Sick Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_sick_pay",
+   "year": 2025,
+   "value": 181849.49628841877,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_sick_pay",
+   "label": "Statutory Sick Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_sick_pay",
+   "year": 2026,
+   "value": 182540.52988168597,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_sick_pay",
+   "label": "Statutory Sick Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_sick_pay",
+   "year": 2027,
+   "value": 183215.93420261145,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_sick_pay",
+   "label": "Statutory Sick Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_sick_pay",
+   "year": 2028,
+   "value": 183948.78862804174,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_sick_pay",
+   "label": "Statutory Sick Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_sick_pay",
+   "year": 2029,
+   "value": 184758.16779726744,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_sick_pay",
+   "label": "Statutory Sick Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_sick_pay",
+   "year": 2030,
+   "value": 185589.58406233788,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_sick_pay",
+   "label": "Statutory Sick Pay"
+  },
+  {
+   "metric_id": "caseloads.statutory_sick_pay",
+   "year": 2031,
+   "value": 186387.61916735768,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "statutory_sick_pay",
+   "label": "Statutory Sick Pay"
+  },
+  {
+   "metric_id": "spending.ssmg",
+   "year": 2025,
+   "value": 13125578.55695486,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "ssmg",
+   "label": "Sure Start Maternity Grant"
+  },
+  {
+   "metric_id": "spending.ssmg",
+   "year": 2026,
+   "value": 13175456.283032894,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "ssmg",
+   "label": "Sure Start Maternity Grant"
+  },
+  {
+   "metric_id": "spending.ssmg",
+   "year": 2027,
+   "value": 13224205.684065819,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "ssmg",
+   "label": "Sure Start Maternity Grant"
+  },
+  {
+   "metric_id": "spending.ssmg",
+   "year": 2028,
+   "value": 13277101.755604148,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "ssmg",
+   "label": "Sure Start Maternity Grant"
+  },
+  {
+   "metric_id": "spending.ssmg",
+   "year": 2029,
+   "value": 13335521.275147796,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "ssmg",
+   "label": "Sure Start Maternity Grant"
+  },
+  {
+   "metric_id": "spending.ssmg",
+   "year": 2030,
+   "value": 13395531.273916364,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "ssmg",
+   "label": "Sure Start Maternity Grant"
+  },
+  {
+   "metric_id": "spending.ssmg",
+   "year": 2031,
+   "value": 13453131.819739938,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "ssmg",
+   "label": "Sure Start Maternity Grant"
+  },
+  {
+   "metric_id": "caseloads.ssmg",
+   "year": 2025,
+   "value": 26251.15711390972,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "ssmg",
+   "label": "Sure Start Maternity Grant"
+  },
+  {
+   "metric_id": "caseloads.ssmg",
+   "year": 2026,
+   "value": 26350.91256606579,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "ssmg",
+   "label": "Sure Start Maternity Grant"
+  },
+  {
+   "metric_id": "caseloads.ssmg",
+   "year": 2027,
+   "value": 26448.411368131638,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "ssmg",
+   "label": "Sure Start Maternity Grant"
+  },
+  {
+   "metric_id": "caseloads.ssmg",
+   "year": 2028,
+   "value": 26554.203511208296,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "ssmg",
+   "label": "Sure Start Maternity Grant"
+  },
+  {
+   "metric_id": "caseloads.ssmg",
+   "year": 2029,
+   "value": 26671.04255029559,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "ssmg",
+   "label": "Sure Start Maternity Grant"
+  },
+  {
+   "metric_id": "caseloads.ssmg",
+   "year": 2030,
+   "value": 26791.062547832727,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "ssmg",
+   "label": "Sure Start Maternity Grant"
+  },
+  {
+   "metric_id": "caseloads.ssmg",
+   "year": 2031,
+   "value": 26906.263639479876,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "ssmg",
+   "label": "Sure Start Maternity Grant"
+  },
+  {
+   "metric_id": "spending.bsp",
+   "year": 2025,
+   "value": 670117436.1645457,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "bsp",
+   "label": "Bereavement Support Payment"
+  },
+  {
+   "metric_id": "spending.bsp",
+   "year": 2026,
+   "value": 688135183.9490275,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "bsp",
+   "label": "Bereavement Support Payment"
+  },
+  {
+   "metric_id": "spending.bsp",
+   "year": 2027,
+   "value": 704494923.9662163,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "bsp",
+   "label": "Bereavement Support Payment"
+  },
+  {
+   "metric_id": "spending.bsp",
+   "year": 2028,
+   "value": 721459099.2289677,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "bsp",
+   "label": "Bereavement Support Payment"
+  },
+  {
+   "metric_id": "spending.bsp",
+   "year": 2029,
+   "value": 739126209.3696653,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "bsp",
+   "label": "Bereavement Support Payment"
+  },
+  {
+   "metric_id": "spending.bsp",
+   "year": 2030,
+   "value": 757301345.0579464,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "bsp",
+   "label": "Bereavement Support Payment"
+  },
+  {
+   "metric_id": "spending.bsp",
+   "year": 2031,
+   "value": 775768909.9039654,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "bsp",
+   "label": "Bereavement Support Payment"
+  },
+  {
+   "metric_id": "caseloads.bsp",
+   "year": 2025,
+   "value": 98107.17104572058,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "bsp",
+   "label": "Bereavement Support Payment"
+  },
+  {
+   "metric_id": "caseloads.bsp",
+   "year": 2026,
+   "value": 98479.98137193918,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "bsp",
+   "label": "Bereavement Support Payment"
+  },
+  {
+   "metric_id": "caseloads.bsp",
+   "year": 2027,
+   "value": 98844.35904490948,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "bsp",
+   "label": "Bereavement Support Payment"
+  },
+  {
+   "metric_id": "caseloads.bsp",
+   "year": 2028,
+   "value": 99239.73120433092,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "bsp",
+   "label": "Bereavement Support Payment"
+  },
+  {
+   "metric_id": "caseloads.bsp",
+   "year": 2029,
+   "value": 99676.38774478436,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "bsp",
+   "label": "Bereavement Support Payment"
+  },
+  {
+   "metric_id": "caseloads.bsp",
+   "year": 2030,
+   "value": 100124.9348872304,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "bsp",
+   "label": "Bereavement Support Payment"
+  },
+  {
+   "metric_id": "caseloads.bsp",
+   "year": 2031,
+   "value": 100555.47150915861,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "bsp",
+   "label": "Bereavement Support Payment"
+  },
+  {
+   "metric_id": "spending.tax_free_childcare",
+   "year": 2025,
+   "value": 551460799.5583543,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "tax_free_childcare",
+   "label": "Tax-Free Childcare"
+  },
+  {
+   "metric_id": "spending.tax_free_childcare",
+   "year": 2026,
+   "value": 556759454.765603,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "tax_free_childcare",
+   "label": "Tax-Free Childcare"
+  },
+  {
+   "metric_id": "spending.tax_free_childcare",
+   "year": 2027,
+   "value": 566094705.6608063,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "tax_free_childcare",
+   "label": "Tax-Free Childcare"
+  },
+  {
+   "metric_id": "spending.tax_free_childcare",
+   "year": 2028,
+   "value": 577814275.3520343,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "tax_free_childcare",
+   "label": "Tax-Free Childcare"
+  },
+  {
+   "metric_id": "spending.tax_free_childcare",
+   "year": 2029,
+   "value": 546986429.725085,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "tax_free_childcare",
+   "label": "Tax-Free Childcare"
+  },
+  {
+   "metric_id": "spending.tax_free_childcare",
+   "year": 2030,
+   "value": 566235601.269679,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "tax_free_childcare",
+   "label": "Tax-Free Childcare"
+  },
+  {
+   "metric_id": "spending.tax_free_childcare",
+   "year": 2031,
+   "value": 577475011.8886111,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "tax_free_childcare",
+   "label": "Tax-Free Childcare"
+  },
+  {
+   "metric_id": "caseloads.tax_free_childcare",
+   "year": 2025,
+   "value": 1011592.7329891622,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "tax_free_childcare",
+   "label": "Tax-Free Childcare"
+  },
+  {
+   "metric_id": "caseloads.tax_free_childcare",
+   "year": 2026,
+   "value": 1021183.6429007053,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "tax_free_childcare",
+   "label": "Tax-Free Childcare"
+  },
+  {
+   "metric_id": "caseloads.tax_free_childcare",
+   "year": 2027,
+   "value": 1020334.1701015085,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "tax_free_childcare",
+   "label": "Tax-Free Childcare"
+  },
+  {
+   "metric_id": "caseloads.tax_free_childcare",
+   "year": 2028,
+   "value": 1023999.8023810685,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "tax_free_childcare",
+   "label": "Tax-Free Childcare"
+  },
+  {
+   "metric_id": "caseloads.tax_free_childcare",
+   "year": 2029,
+   "value": 972793.2253160179,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "tax_free_childcare",
+   "label": "Tax-Free Childcare"
+  },
+  {
+   "metric_id": "caseloads.tax_free_childcare",
+   "year": 2030,
+   "value": 980748.0028229207,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "tax_free_childcare",
+   "label": "Tax-Free Childcare"
+  },
+  {
+   "metric_id": "caseloads.tax_free_childcare",
+   "year": 2031,
+   "value": 980979.6002974808,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "tax_free_childcare",
+   "label": "Tax-Free Childcare"
+  },
+  {
+   "metric_id": "spending.extended_childcare_entitlement",
+   "year": 2025,
+   "value": 2648748217.184513,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "extended_childcare_entitlement",
+   "label": "Extended childcare entitlement"
+  },
+  {
+   "metric_id": "spending.extended_childcare_entitlement",
+   "year": 2026,
+   "value": 3074999356.8385277,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "extended_childcare_entitlement",
+   "label": "Extended childcare entitlement"
+  },
+  {
+   "metric_id": "spending.extended_childcare_entitlement",
+   "year": 2027,
+   "value": 3151701029.7253556,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "extended_childcare_entitlement",
+   "label": "Extended childcare entitlement"
+  },
+  {
+   "metric_id": "spending.extended_childcare_entitlement",
+   "year": 2028,
+   "value": 3235525477.42588,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "extended_childcare_entitlement",
+   "label": "Extended childcare entitlement"
+  },
+  {
+   "metric_id": "spending.extended_childcare_entitlement",
+   "year": 2029,
+   "value": 3310370308.099014,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "extended_childcare_entitlement",
+   "label": "Extended childcare entitlement"
+  },
+  {
+   "metric_id": "spending.extended_childcare_entitlement",
+   "year": 2030,
+   "value": 3398248782.2036104,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "extended_childcare_entitlement",
+   "label": "Extended childcare entitlement"
+  },
+  {
+   "metric_id": "spending.extended_childcare_entitlement",
+   "year": 2031,
+   "value": 3482394959.2131405,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "extended_childcare_entitlement",
+   "label": "Extended childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.extended_childcare_entitlement",
+   "year": 2025,
+   "value": 664320.1943173409,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "extended_childcare_entitlement",
+   "label": "Extended childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.extended_childcare_entitlement",
+   "year": 2026,
+   "value": 669802.4551296234,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "extended_childcare_entitlement",
+   "label": "Extended childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.extended_childcare_entitlement",
+   "year": 2027,
+   "value": 672223.7327985764,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "extended_childcare_entitlement",
+   "label": "Extended childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.extended_childcare_entitlement",
+   "year": 2028,
+   "value": 673895.0757799149,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "extended_childcare_entitlement",
+   "label": "Extended childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.extended_childcare_entitlement",
+   "year": 2029,
+   "value": 676376.3725852966,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "extended_childcare_entitlement",
+   "label": "Extended childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.extended_childcare_entitlement",
+   "year": 2030,
+   "value": 681117.3323864937,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "extended_childcare_entitlement",
+   "label": "Extended childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.extended_childcare_entitlement",
+   "year": 2031,
+   "value": 683408.1116919518,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "extended_childcare_entitlement",
+   "label": "Extended childcare entitlement"
+  },
+  {
+   "metric_id": "spending.universal_childcare_entitlement",
+   "year": 2025,
+   "value": 3266472521.328236,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "universal_childcare_entitlement",
+   "label": "Universal childcare entitlement"
+  },
+  {
+   "metric_id": "spending.universal_childcare_entitlement",
+   "year": 2026,
+   "value": 3388720332.016673,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "universal_childcare_entitlement",
+   "label": "Universal childcare entitlement"
+  },
+  {
+   "metric_id": "spending.universal_childcare_entitlement",
+   "year": 2027,
+   "value": 3479027407.998313,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "universal_childcare_entitlement",
+   "label": "Universal childcare entitlement"
+  },
+  {
+   "metric_id": "spending.universal_childcare_entitlement",
+   "year": 2028,
+   "value": 3565630277.573733,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "universal_childcare_entitlement",
+   "label": "Universal childcare entitlement"
+  },
+  {
+   "metric_id": "spending.universal_childcare_entitlement",
+   "year": 2029,
+   "value": 3653067234.422063,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "universal_childcare_entitlement",
+   "label": "Universal childcare entitlement"
+  },
+  {
+   "metric_id": "spending.universal_childcare_entitlement",
+   "year": 2030,
+   "value": 3757717185.8182206,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "universal_childcare_entitlement",
+   "label": "Universal childcare entitlement"
+  },
+  {
+   "metric_id": "spending.universal_childcare_entitlement",
+   "year": 2031,
+   "value": 3846294804.2724795,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "universal_childcare_entitlement",
+   "label": "Universal childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.universal_childcare_entitlement",
+   "year": 2025,
+   "value": 985445.4297113419,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "universal_childcare_entitlement",
+   "label": "Universal childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.universal_childcare_entitlement",
+   "year": 2026,
+   "value": 988718.9267745018,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "universal_childcare_entitlement",
+   "label": "Universal childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.universal_childcare_entitlement",
+   "year": 2027,
+   "value": 992135.8684687614,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "universal_childcare_entitlement",
+   "label": "Universal childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.universal_childcare_entitlement",
+   "year": 2028,
+   "value": 996884.7139797211,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "universal_childcare_entitlement",
+   "label": "Universal childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.universal_childcare_entitlement",
+   "year": 2029,
+   "value": 1001194.633936882,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "universal_childcare_entitlement",
+   "label": "Universal childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.universal_childcare_entitlement",
+   "year": 2030,
+   "value": 1010204.9373459816,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "universal_childcare_entitlement",
+   "label": "Universal childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.universal_childcare_entitlement",
+   "year": 2031,
+   "value": 1013771.0637845993,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "universal_childcare_entitlement",
+   "label": "Universal childcare entitlement"
+  },
+  {
+   "metric_id": "spending.targeted_childcare_entitlement",
+   "year": 2025,
+   "value": 572024783.1285633,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "targeted_childcare_entitlement",
+   "label": "Targeted childcare entitlement"
+  },
+  {
+   "metric_id": "spending.targeted_childcare_entitlement",
+   "year": 2026,
+   "value": 593721428.6371526,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "targeted_childcare_entitlement",
+   "label": "Targeted childcare entitlement"
+  },
+  {
+   "metric_id": "spending.targeted_childcare_entitlement",
+   "year": 2027,
+   "value": 609623032.498304,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "targeted_childcare_entitlement",
+   "label": "Targeted childcare entitlement"
+  },
+  {
+   "metric_id": "spending.targeted_childcare_entitlement",
+   "year": 2028,
+   "value": 624301765.4036878,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "targeted_childcare_entitlement",
+   "label": "Targeted childcare entitlement"
+  },
+  {
+   "metric_id": "spending.targeted_childcare_entitlement",
+   "year": 2029,
+   "value": 639589086.6096659,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "targeted_childcare_entitlement",
+   "label": "Targeted childcare entitlement"
+  },
+  {
+   "metric_id": "spending.targeted_childcare_entitlement",
+   "year": 2030,
+   "value": 655315201.2710408,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "targeted_childcare_entitlement",
+   "label": "Targeted childcare entitlement"
+  },
+  {
+   "metric_id": "spending.targeted_childcare_entitlement",
+   "year": 2031,
+   "value": 664824494.3626773,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "targeted_childcare_entitlement",
+   "label": "Targeted childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.targeted_childcare_entitlement",
+   "year": 2025,
+   "value": 119291.62903356552,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "targeted_childcare_entitlement",
+   "label": "Targeted childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.targeted_childcare_entitlement",
+   "year": 2026,
+   "value": 119744.94127082825,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "targeted_childcare_entitlement",
+   "label": "Targeted childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.targeted_childcare_entitlement",
+   "year": 2027,
+   "value": 120188.00064563751,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "targeted_childcare_entitlement",
+   "label": "Targeted childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.targeted_childcare_entitlement",
+   "year": 2028,
+   "value": 120668.74804401398,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "targeted_childcare_entitlement",
+   "label": "Targeted childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.targeted_childcare_entitlement",
+   "year": 2029,
+   "value": 121199.69331502914,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "targeted_childcare_entitlement",
+   "label": "Targeted childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.targeted_childcare_entitlement",
+   "year": 2030,
+   "value": 121745.09483098984,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "targeted_childcare_entitlement",
+   "label": "Targeted childcare entitlement"
+  },
+  {
+   "metric_id": "caseloads.targeted_childcare_entitlement",
+   "year": 2031,
+   "value": 121100.23704528809,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "targeted_childcare_entitlement",
+   "label": "Targeted childcare entitlement"
+  },
+  {
+   "metric_id": "spending.care_to_learn",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "care_to_learn",
+   "label": "Care to Learn"
+  },
+  {
+   "metric_id": "spending.care_to_learn",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "care_to_learn",
+   "label": "Care to Learn"
+  },
+  {
+   "metric_id": "spending.care_to_learn",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "care_to_learn",
+   "label": "Care to Learn"
+  },
+  {
+   "metric_id": "spending.care_to_learn",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "care_to_learn",
+   "label": "Care to Learn"
+  },
+  {
+   "metric_id": "spending.care_to_learn",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "care_to_learn",
+   "label": "Care to Learn"
+  },
+  {
+   "metric_id": "spending.care_to_learn",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "care_to_learn",
+   "label": "Care to Learn"
+  },
+  {
+   "metric_id": "spending.care_to_learn",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "care_to_learn",
+   "label": "Care to Learn"
+  },
+  {
+   "metric_id": "caseloads.care_to_learn",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "care_to_learn",
+   "label": "Care to Learn"
+  },
+  {
+   "metric_id": "caseloads.care_to_learn",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "care_to_learn",
+   "label": "Care to Learn"
+  },
+  {
+   "metric_id": "caseloads.care_to_learn",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "care_to_learn",
+   "label": "Care to Learn"
+  },
+  {
+   "metric_id": "caseloads.care_to_learn",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "care_to_learn",
+   "label": "Care to Learn"
+  },
+  {
+   "metric_id": "caseloads.care_to_learn",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "care_to_learn",
+   "label": "Care to Learn"
+  },
+  {
+   "metric_id": "caseloads.care_to_learn",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "care_to_learn",
+   "label": "Care to Learn"
+  },
+  {
+   "metric_id": "caseloads.care_to_learn",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "care_to_learn",
+   "label": "Care to Learn"
+  },
+  {
+   "metric_id": "spending.childcare_grant",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "childcare_grant",
+   "label": "Childcare Grant"
+  },
+  {
+   "metric_id": "spending.childcare_grant",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "childcare_grant",
+   "label": "Childcare Grant"
+  },
+  {
+   "metric_id": "spending.childcare_grant",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "childcare_grant",
+   "label": "Childcare Grant"
+  },
+  {
+   "metric_id": "spending.childcare_grant",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "childcare_grant",
+   "label": "Childcare Grant"
+  },
+  {
+   "metric_id": "spending.childcare_grant",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "childcare_grant",
+   "label": "Childcare Grant"
+  },
+  {
+   "metric_id": "spending.childcare_grant",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "childcare_grant",
+   "label": "Childcare Grant"
+  },
+  {
+   "metric_id": "spending.childcare_grant",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "childcare_grant",
+   "label": "Childcare Grant"
+  },
+  {
+   "metric_id": "caseloads.childcare_grant",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "childcare_grant",
+   "label": "Childcare Grant"
+  },
+  {
+   "metric_id": "caseloads.childcare_grant",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "childcare_grant",
+   "label": "Childcare Grant"
+  },
+  {
+   "metric_id": "caseloads.childcare_grant",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "childcare_grant",
+   "label": "Childcare Grant"
+  },
+  {
+   "metric_id": "caseloads.childcare_grant",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "childcare_grant",
+   "label": "Childcare Grant"
+  },
+  {
+   "metric_id": "caseloads.childcare_grant",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "childcare_grant",
+   "label": "Childcare Grant"
+  },
+  {
+   "metric_id": "caseloads.childcare_grant",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "childcare_grant",
+   "label": "Childcare Grant"
+  },
+  {
+   "metric_id": "caseloads.childcare_grant",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "childcare_grant",
+   "label": "Childcare Grant"
+  },
+  {
+   "metric_id": "spending.parents_learning_allowance",
+   "year": 2025,
+   "value": 199163916.0120163,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "parents_learning_allowance",
+   "label": "Parents' Learning Allowance"
+  },
+  {
+   "metric_id": "spending.parents_learning_allowance",
+   "year": 2026,
+   "value": 199920745.4944954,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "parents_learning_allowance",
+   "label": "Parents' Learning Allowance"
+  },
+  {
+   "metric_id": "spending.parents_learning_allowance",
+   "year": 2027,
+   "value": 200660457.07904816,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "parents_learning_allowance",
+   "label": "Parents' Learning Allowance"
+  },
+  {
+   "metric_id": "spending.parents_learning_allowance",
+   "year": 2028,
+   "value": 201463089.27254868,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "parents_learning_allowance",
+   "label": "Parents' Learning Allowance"
+  },
+  {
+   "metric_id": "spending.parents_learning_allowance",
+   "year": 2029,
+   "value": 202349531.5649414,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "parents_learning_allowance",
+   "label": "Parents' Learning Allowance"
+  },
+  {
+   "metric_id": "spending.parents_learning_allowance",
+   "year": 2030,
+   "value": 203188174.47846603,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "parents_learning_allowance",
+   "label": "Parents' Learning Allowance"
+  },
+  {
+   "metric_id": "spending.parents_learning_allowance",
+   "year": 2031,
+   "value": 204061882.82292938,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "parents_learning_allowance",
+   "label": "Parents' Learning Allowance"
+  },
+  {
+   "metric_id": "caseloads.parents_learning_allowance",
+   "year": 2025,
+   "value": 98401.14427471161,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "parents_learning_allowance",
+   "label": "Parents' Learning Allowance"
+  },
+  {
+   "metric_id": "caseloads.parents_learning_allowance",
+   "year": 2026,
+   "value": 98775.07188463211,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "parents_learning_allowance",
+   "label": "Parents' Learning Allowance"
+  },
+  {
+   "metric_id": "caseloads.parents_learning_allowance",
+   "year": 2027,
+   "value": 99140.54203510284,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "parents_learning_allowance",
+   "label": "Parents' Learning Allowance"
+  },
+  {
+   "metric_id": "caseloads.parents_learning_allowance",
+   "year": 2028,
+   "value": 99537.09944295883,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "parents_learning_allowance",
+   "label": "Parents' Learning Allowance"
+  },
+  {
+   "metric_id": "caseloads.parents_learning_allowance",
+   "year": 2029,
+   "value": 99975.0650024414,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "parents_learning_allowance",
+   "label": "Parents' Learning Allowance"
+  },
+  {
+   "metric_id": "caseloads.parents_learning_allowance",
+   "year": 2030,
+   "value": 100389.41426801682,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "parents_learning_allowance",
+   "label": "Parents' Learning Allowance"
+  },
+  {
+   "metric_id": "caseloads.parents_learning_allowance",
+   "year": 2031,
+   "value": 100821.0883512497,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "parents_learning_allowance",
+   "label": "Parents' Learning Allowance"
+  },
+  {
+   "metric_id": "spending.adult_dependants_grant",
+   "year": 2025,
+   "value": 62362242.98413515,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "adult_dependants_grant",
+   "label": "Adult Dependants' Grant"
+  },
+  {
+   "metric_id": "spending.adult_dependants_grant",
+   "year": 2026,
+   "value": 62599222.553822994,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "adult_dependants_grant",
+   "label": "Adult Dependants' Grant"
+  },
+  {
+   "metric_id": "spending.adult_dependants_grant",
+   "year": 2027,
+   "value": 44187742.20506668,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "adult_dependants_grant",
+   "label": "Adult Dependants' Grant"
+  },
+  {
+   "metric_id": "spending.adult_dependants_grant",
+   "year": 2028,
+   "value": 44364490.80733061,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "adult_dependants_grant",
+   "label": "Adult Dependants' Grant"
+  },
+  {
+   "metric_id": "spending.adult_dependants_grant",
+   "year": 2029,
+   "value": 44559696.51651859,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "adult_dependants_grant",
+   "label": "Adult Dependants' Grant"
+  },
+  {
+   "metric_id": "spending.adult_dependants_grant",
+   "year": 2030,
+   "value": 44760217.169840336,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "adult_dependants_grant",
+   "label": "Adult Dependants' Grant"
+  },
+  {
+   "metric_id": "spending.adult_dependants_grant",
+   "year": 2031,
+   "value": 44952685.864076614,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "adult_dependants_grant",
+   "label": "Adult Dependants' Grant"
+  },
+  {
+   "metric_id": "caseloads.adult_dependants_grant",
+   "year": 2025,
+   "value": 17591.605919361115,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "adult_dependants_grant",
+   "label": "Adult Dependants' Grant"
+  },
+  {
+   "metric_id": "caseloads.adult_dependants_grant",
+   "year": 2026,
+   "value": 17658.454881191254,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "adult_dependants_grant",
+   "label": "Adult Dependants' Grant"
+  },
+  {
+   "metric_id": "caseloads.adult_dependants_grant",
+   "year": 2027,
+   "value": 12464.807392120361,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "adult_dependants_grant",
+   "label": "Adult Dependants' Grant"
+  },
+  {
+   "metric_id": "caseloads.adult_dependants_grant",
+   "year": 2028,
+   "value": 12514.665954113007,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "adult_dependants_grant",
+   "label": "Adult Dependants' Grant"
+  },
+  {
+   "metric_id": "caseloads.adult_dependants_grant",
+   "year": 2029,
+   "value": 12569.73103427887,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "adult_dependants_grant",
+   "label": "Adult Dependants' Grant"
+  },
+  {
+   "metric_id": "caseloads.adult_dependants_grant",
+   "year": 2030,
+   "value": 12626.29539346695,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "adult_dependants_grant",
+   "label": "Adult Dependants' Grant"
+  },
+  {
+   "metric_id": "caseloads.adult_dependants_grant",
+   "year": 2031,
+   "value": 12680.588396072388,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "adult_dependants_grant",
+   "label": "Adult Dependants' Grant"
+  },
+  {
+   "metric_id": "spending.travel_grant",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "travel_grant",
+   "label": "Travel Grant"
+  },
+  {
+   "metric_id": "spending.travel_grant",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "travel_grant",
+   "label": "Travel Grant"
+  },
+  {
+   "metric_id": "spending.travel_grant",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "travel_grant",
+   "label": "Travel Grant"
+  },
+  {
+   "metric_id": "spending.travel_grant",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "travel_grant",
+   "label": "Travel Grant"
+  },
+  {
+   "metric_id": "spending.travel_grant",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "travel_grant",
+   "label": "Travel Grant"
+  },
+  {
+   "metric_id": "spending.travel_grant",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "travel_grant",
+   "label": "Travel Grant"
+  },
+  {
+   "metric_id": "spending.travel_grant",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "travel_grant",
+   "label": "Travel Grant"
+  },
+  {
+   "metric_id": "caseloads.travel_grant",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "travel_grant",
+   "label": "Travel Grant"
+  },
+  {
+   "metric_id": "caseloads.travel_grant",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "travel_grant",
+   "label": "Travel Grant"
+  },
+  {
+   "metric_id": "caseloads.travel_grant",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "travel_grant",
+   "label": "Travel Grant"
+  },
+  {
+   "metric_id": "caseloads.travel_grant",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "travel_grant",
+   "label": "Travel Grant"
+  },
+  {
+   "metric_id": "caseloads.travel_grant",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "travel_grant",
+   "label": "Travel Grant"
+  },
+  {
+   "metric_id": "caseloads.travel_grant",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "travel_grant",
+   "label": "Travel Grant"
+  },
+  {
+   "metric_id": "caseloads.travel_grant",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "travel_grant",
+   "label": "Travel Grant"
+  },
+  {
+   "metric_id": "spending.disabled_students_allowance",
+   "year": 2025,
+   "value": null,
+   "suppressed": true,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "disabled_students_allowance",
+   "label": "Disabled Students' Allowance"
+  },
+  {
+   "metric_id": "spending.disabled_students_allowance",
+   "year": 2026,
+   "value": null,
+   "suppressed": true,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "disabled_students_allowance",
+   "label": "Disabled Students' Allowance"
+  },
+  {
+   "metric_id": "spending.disabled_students_allowance",
+   "year": 2027,
+   "value": null,
+   "suppressed": true,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "disabled_students_allowance",
+   "label": "Disabled Students' Allowance"
+  },
+  {
+   "metric_id": "spending.disabled_students_allowance",
+   "year": 2028,
+   "value": null,
+   "suppressed": true,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "disabled_students_allowance",
+   "label": "Disabled Students' Allowance"
+  },
+  {
+   "metric_id": "spending.disabled_students_allowance",
+   "year": 2029,
+   "value": null,
+   "suppressed": true,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "disabled_students_allowance",
+   "label": "Disabled Students' Allowance"
+  },
+  {
+   "metric_id": "spending.disabled_students_allowance",
+   "year": 2030,
+   "value": null,
+   "suppressed": true,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "disabled_students_allowance",
+   "label": "Disabled Students' Allowance"
+  },
+  {
+   "metric_id": "spending.disabled_students_allowance",
+   "year": 2031,
+   "value": null,
+   "suppressed": true,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "disabled_students_allowance",
+   "label": "Disabled Students' Allowance"
+  },
+  {
+   "metric_id": "caseloads.disabled_students_allowance",
+   "year": 2025,
+   "value": null,
+   "suppressed": true,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "disabled_students_allowance",
+   "label": "Disabled Students' Allowance"
+  },
+  {
+   "metric_id": "caseloads.disabled_students_allowance",
+   "year": 2026,
+   "value": null,
+   "suppressed": true,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "disabled_students_allowance",
+   "label": "Disabled Students' Allowance"
+  },
+  {
+   "metric_id": "caseloads.disabled_students_allowance",
+   "year": 2027,
+   "value": null,
+   "suppressed": true,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "disabled_students_allowance",
+   "label": "Disabled Students' Allowance"
+  },
+  {
+   "metric_id": "caseloads.disabled_students_allowance",
+   "year": 2028,
+   "value": null,
+   "suppressed": true,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "disabled_students_allowance",
+   "label": "Disabled Students' Allowance"
+  },
+  {
+   "metric_id": "caseloads.disabled_students_allowance",
+   "year": 2029,
+   "value": null,
+   "suppressed": true,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "disabled_students_allowance",
+   "label": "Disabled Students' Allowance"
+  },
+  {
+   "metric_id": "caseloads.disabled_students_allowance",
+   "year": 2030,
+   "value": null,
+   "suppressed": true,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "disabled_students_allowance",
+   "label": "Disabled Students' Allowance"
+  },
+  {
+   "metric_id": "caseloads.disabled_students_allowance",
+   "year": 2031,
+   "value": null,
+   "suppressed": true,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "disabled_students_allowance",
+   "label": "Disabled Students' Allowance"
+  },
+  {
+   "metric_id": "spending.bursary_fund_16_to_19",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "bursary_fund_16_to_19",
+   "label": "16 to 19 Bursary Fund"
+  },
+  {
+   "metric_id": "spending.bursary_fund_16_to_19",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "bursary_fund_16_to_19",
+   "label": "16 to 19 Bursary Fund"
+  },
+  {
+   "metric_id": "spending.bursary_fund_16_to_19",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "bursary_fund_16_to_19",
+   "label": "16 to 19 Bursary Fund"
+  },
+  {
+   "metric_id": "spending.bursary_fund_16_to_19",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "bursary_fund_16_to_19",
+   "label": "16 to 19 Bursary Fund"
+  },
+  {
+   "metric_id": "spending.bursary_fund_16_to_19",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "bursary_fund_16_to_19",
+   "label": "16 to 19 Bursary Fund"
+  },
+  {
+   "metric_id": "spending.bursary_fund_16_to_19",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "bursary_fund_16_to_19",
+   "label": "16 to 19 Bursary Fund"
+  },
+  {
+   "metric_id": "spending.bursary_fund_16_to_19",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "bursary_fund_16_to_19",
+   "label": "16 to 19 Bursary Fund"
+  },
+  {
+   "metric_id": "caseloads.bursary_fund_16_to_19",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "bursary_fund_16_to_19",
+   "label": "16 to 19 Bursary Fund"
+  },
+  {
+   "metric_id": "caseloads.bursary_fund_16_to_19",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "bursary_fund_16_to_19",
+   "label": "16 to 19 Bursary Fund"
+  },
+  {
+   "metric_id": "caseloads.bursary_fund_16_to_19",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "bursary_fund_16_to_19",
+   "label": "16 to 19 Bursary Fund"
+  },
+  {
+   "metric_id": "caseloads.bursary_fund_16_to_19",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "bursary_fund_16_to_19",
+   "label": "16 to 19 Bursary Fund"
+  },
+  {
+   "metric_id": "caseloads.bursary_fund_16_to_19",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "bursary_fund_16_to_19",
+   "label": "16 to 19 Bursary Fund"
+  },
+  {
+   "metric_id": "caseloads.bursary_fund_16_to_19",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "bursary_fund_16_to_19",
+   "label": "16 to 19 Bursary Fund"
+  },
+  {
+   "metric_id": "caseloads.bursary_fund_16_to_19",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "bursary_fund_16_to_19",
+   "label": "16 to 19 Bursary Fund"
+  },
+  {
+   "metric_id": "spending.nhs_spending",
+   "year": 2025,
+   "value": 204681602159.08142,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "nhs_spending",
+   "label": "NHS spending"
+  },
+  {
+   "metric_id": "spending.nhs_spending",
+   "year": 2026,
+   "value": 205459399297.22324,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "nhs_spending",
+   "label": "NHS spending"
+  },
+  {
+   "metric_id": "spending.nhs_spending",
+   "year": 2027,
+   "value": 206219602604.32385,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "nhs_spending",
+   "label": "NHS spending"
+  },
+  {
+   "metric_id": "spending.nhs_spending",
+   "year": 2028,
+   "value": 207044470471.60086,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "nhs_spending",
+   "label": "NHS spending"
+  },
+  {
+   "metric_id": "spending.nhs_spending",
+   "year": 2029,
+   "value": 207955469002.00323,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "nhs_spending",
+   "label": "NHS spending"
+  },
+  {
+   "metric_id": "spending.nhs_spending",
+   "year": 2030,
+   "value": 208891275075.5078,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "nhs_spending",
+   "label": "NHS spending"
+  },
+  {
+   "metric_id": "spending.nhs_spending",
+   "year": 2031,
+   "value": 209789506836.96567,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "nhs_spending",
+   "label": "NHS spending"
+  },
+  {
+   "metric_id": "spending.dfe_education_spending",
+   "year": 2025,
+   "value": 23991604.347275984,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dfe_education_spending",
+   "label": "State education spending"
+  },
+  {
+   "metric_id": "spending.dfe_education_spending",
+   "year": 2026,
+   "value": 24082773.488280036,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dfe_education_spending",
+   "label": "State education spending"
+  },
+  {
+   "metric_id": "spending.dfe_education_spending",
+   "year": 2027,
+   "value": 24171879.76151417,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dfe_education_spending",
+   "label": "State education spending"
+  },
+  {
+   "metric_id": "spending.dfe_education_spending",
+   "year": 2028,
+   "value": 24268565.8560597,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dfe_education_spending",
+   "label": "State education spending"
+  },
+  {
+   "metric_id": "spending.dfe_education_spending",
+   "year": 2029,
+   "value": 24375348.2293824,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dfe_education_spending",
+   "label": "State education spending"
+  },
+  {
+   "metric_id": "spending.dfe_education_spending",
+   "year": 2030,
+   "value": 24485037.725156102,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dfe_education_spending",
+   "label": "State education spending"
+  },
+  {
+   "metric_id": "spending.dfe_education_spending",
+   "year": 2031,
+   "value": 24590323.320610803,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dfe_education_spending",
+   "label": "State education spending"
+  },
+  {
+   "metric_id": "spending.dft_subsidy_spending",
+   "year": 2025,
+   "value": 25151126494.365833,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dft_subsidy_spending",
+   "label": "Transport subsidy spending"
+  },
+  {
+   "metric_id": "spending.dft_subsidy_spending",
+   "year": 2026,
+   "value": 25246701555.2342,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dft_subsidy_spending",
+   "label": "Transport subsidy spending"
+  },
+  {
+   "metric_id": "spending.dft_subsidy_spending",
+   "year": 2027,
+   "value": 25340114784.780163,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dft_subsidy_spending",
+   "label": "Transport subsidy spending"
+  },
+  {
+   "metric_id": "spending.dft_subsidy_spending",
+   "year": 2028,
+   "value": 25441473946.770885,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dft_subsidy_spending",
+   "label": "Transport subsidy spending"
+  },
+  {
+   "metric_id": "spending.dft_subsidy_spending",
+   "year": 2029,
+   "value": 25553416805.737003,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dft_subsidy_spending",
+   "label": "Transport subsidy spending"
+  },
+  {
+   "metric_id": "spending.dft_subsidy_spending",
+   "year": 2030,
+   "value": 25668407910.982876,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dft_subsidy_spending",
+   "label": "Transport subsidy spending"
+  },
+  {
+   "metric_id": "spending.dft_subsidy_spending",
+   "year": 2031,
+   "value": 25778782013.14279,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "dft_subsidy_spending",
+   "label": "Transport subsidy spending"
+  },
+  {
+   "metric_id": "spending.cost_of_living_support_payment",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "cost_of_living_support_payment",
+   "label": "Cost-of-living support payment"
+  },
+  {
+   "metric_id": "spending.cost_of_living_support_payment",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "cost_of_living_support_payment",
+   "label": "Cost-of-living support payment"
+  },
+  {
+   "metric_id": "spending.cost_of_living_support_payment",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "cost_of_living_support_payment",
+   "label": "Cost-of-living support payment"
+  },
+  {
+   "metric_id": "spending.cost_of_living_support_payment",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "cost_of_living_support_payment",
+   "label": "Cost-of-living support payment"
+  },
+  {
+   "metric_id": "spending.cost_of_living_support_payment",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "cost_of_living_support_payment",
+   "label": "Cost-of-living support payment"
+  },
+  {
+   "metric_id": "spending.cost_of_living_support_payment",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "cost_of_living_support_payment",
+   "label": "Cost-of-living support payment"
+  },
+  {
+   "metric_id": "spending.cost_of_living_support_payment",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "cost_of_living_support_payment",
+   "label": "Cost-of-living support payment"
+  },
+  {
+   "metric_id": "caseloads.cost_of_living_support_payment",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "cost_of_living_support_payment",
+   "label": "Cost-of-living support payment"
+  },
+  {
+   "metric_id": "caseloads.cost_of_living_support_payment",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "cost_of_living_support_payment",
+   "label": "Cost-of-living support payment"
+  },
+  {
+   "metric_id": "caseloads.cost_of_living_support_payment",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "cost_of_living_support_payment",
+   "label": "Cost-of-living support payment"
+  },
+  {
+   "metric_id": "caseloads.cost_of_living_support_payment",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "cost_of_living_support_payment",
+   "label": "Cost-of-living support payment"
+  },
+  {
+   "metric_id": "caseloads.cost_of_living_support_payment",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "cost_of_living_support_payment",
+   "label": "Cost-of-living support payment"
+  },
+  {
+   "metric_id": "caseloads.cost_of_living_support_payment",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "cost_of_living_support_payment",
+   "label": "Cost-of-living support payment"
+  },
+  {
+   "metric_id": "caseloads.cost_of_living_support_payment",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "cost_of_living_support_payment",
+   "label": "Cost-of-living support payment"
+  },
+  {
+   "metric_id": "spending.energy_bills_rebate",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "energy_bills_rebate",
+   "label": "Energy Bills Rebate"
+  },
+  {
+   "metric_id": "spending.energy_bills_rebate",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "energy_bills_rebate",
+   "label": "Energy Bills Rebate"
+  },
+  {
+   "metric_id": "spending.energy_bills_rebate",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "energy_bills_rebate",
+   "label": "Energy Bills Rebate"
+  },
+  {
+   "metric_id": "spending.energy_bills_rebate",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "energy_bills_rebate",
+   "label": "Energy Bills Rebate"
+  },
+  {
+   "metric_id": "spending.energy_bills_rebate",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "energy_bills_rebate",
+   "label": "Energy Bills Rebate"
+  },
+  {
+   "metric_id": "spending.energy_bills_rebate",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "energy_bills_rebate",
+   "label": "Energy Bills Rebate"
+  },
+  {
+   "metric_id": "spending.energy_bills_rebate",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "energy_bills_rebate",
+   "label": "Energy Bills Rebate"
+  },
+  {
+   "metric_id": "caseloads.energy_bills_rebate",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "energy_bills_rebate",
+   "label": "Energy Bills Rebate"
+  },
+  {
+   "metric_id": "caseloads.energy_bills_rebate",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "energy_bills_rebate",
+   "label": "Energy Bills Rebate"
+  },
+  {
+   "metric_id": "caseloads.energy_bills_rebate",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "energy_bills_rebate",
+   "label": "Energy Bills Rebate"
+  },
+  {
+   "metric_id": "caseloads.energy_bills_rebate",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "energy_bills_rebate",
+   "label": "Energy Bills Rebate"
+  },
+  {
+   "metric_id": "caseloads.energy_bills_rebate",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "energy_bills_rebate",
+   "label": "Energy Bills Rebate"
+  },
+  {
+   "metric_id": "caseloads.energy_bills_rebate",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "energy_bills_rebate",
+   "label": "Energy Bills Rebate"
+  },
+  {
+   "metric_id": "caseloads.energy_bills_rebate",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "energy_bills_rebate",
+   "label": "Energy Bills Rebate"
+  },
+  {
+   "metric_id": "spending.epg_subsidy",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "epg_subsidy",
+   "label": "Energy Price Guarantee subsidy"
+  },
+  {
+   "metric_id": "spending.epg_subsidy",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "epg_subsidy",
+   "label": "Energy Price Guarantee subsidy"
+  },
+  {
+   "metric_id": "spending.epg_subsidy",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "epg_subsidy",
+   "label": "Energy Price Guarantee subsidy"
+  },
+  {
+   "metric_id": "spending.epg_subsidy",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "epg_subsidy",
+   "label": "Energy Price Guarantee subsidy"
+  },
+  {
+   "metric_id": "spending.epg_subsidy",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "epg_subsidy",
+   "label": "Energy Price Guarantee subsidy"
+  },
+  {
+   "metric_id": "spending.epg_subsidy",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "epg_subsidy",
+   "label": "Energy Price Guarantee subsidy"
+  },
+  {
+   "metric_id": "spending.epg_subsidy",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "epg_subsidy",
+   "label": "Energy Price Guarantee subsidy"
+  },
+  {
+   "metric_id": "caseloads.epg_subsidy",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "epg_subsidy",
+   "label": "Energy Price Guarantee subsidy"
+  },
+  {
+   "metric_id": "caseloads.epg_subsidy",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "epg_subsidy",
+   "label": "Energy Price Guarantee subsidy"
+  },
+  {
+   "metric_id": "caseloads.epg_subsidy",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "epg_subsidy",
+   "label": "Energy Price Guarantee subsidy"
+  },
+  {
+   "metric_id": "caseloads.epg_subsidy",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "epg_subsidy",
+   "label": "Energy Price Guarantee subsidy"
+  },
+  {
+   "metric_id": "caseloads.epg_subsidy",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "epg_subsidy",
+   "label": "Energy Price Guarantee subsidy"
+  },
+  {
+   "metric_id": "caseloads.epg_subsidy",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "epg_subsidy",
+   "label": "Energy Price Guarantee subsidy"
+  },
+  {
+   "metric_id": "caseloads.epg_subsidy",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "epg_subsidy",
+   "label": "Energy Price Guarantee subsidy"
+  },
+  {
+   "metric_id": "spending.basic_income",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "basic_income",
+   "label": "Basic income"
+  },
+  {
+   "metric_id": "spending.basic_income",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "basic_income",
+   "label": "Basic income"
+  },
+  {
+   "metric_id": "spending.basic_income",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "basic_income",
+   "label": "Basic income"
+  },
+  {
+   "metric_id": "spending.basic_income",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "basic_income",
+   "label": "Basic income"
+  },
+  {
+   "metric_id": "spending.basic_income",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "basic_income",
+   "label": "Basic income"
+  },
+  {
+   "metric_id": "spending.basic_income",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "basic_income",
+   "label": "Basic income"
+  },
+  {
+   "metric_id": "spending.basic_income",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "basic_income",
+   "label": "Basic income"
+  },
+  {
+   "metric_id": "caseloads.basic_income",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "basic_income",
+   "label": "Basic income"
+  },
+  {
+   "metric_id": "caseloads.basic_income",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "basic_income",
+   "label": "Basic income"
+  },
+  {
+   "metric_id": "caseloads.basic_income",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "basic_income",
+   "label": "Basic income"
+  },
+  {
+   "metric_id": "caseloads.basic_income",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "basic_income",
+   "label": "Basic income"
+  },
+  {
+   "metric_id": "caseloads.basic_income",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "basic_income",
+   "label": "Basic income"
+  },
+  {
+   "metric_id": "caseloads.basic_income",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "basic_income",
+   "label": "Basic income"
+  },
+  {
+   "metric_id": "caseloads.basic_income",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "basic_income",
+   "label": "Basic income"
+  },
+  {
+   "metric_id": "spending.disability_basic_income",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "disability_basic_income",
+   "label": "Disability basic income"
+  },
+  {
+   "metric_id": "spending.disability_basic_income",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "disability_basic_income",
+   "label": "Disability basic income"
+  },
+  {
+   "metric_id": "spending.disability_basic_income",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "disability_basic_income",
+   "label": "Disability basic income"
+  },
+  {
+   "metric_id": "spending.disability_basic_income",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "disability_basic_income",
+   "label": "Disability basic income"
+  },
+  {
+   "metric_id": "spending.disability_basic_income",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "disability_basic_income",
+   "label": "Disability basic income"
+  },
+  {
+   "metric_id": "spending.disability_basic_income",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "disability_basic_income",
+   "label": "Disability basic income"
+  },
+  {
+   "metric_id": "spending.disability_basic_income",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "disability_basic_income",
+   "label": "Disability basic income"
+  },
+  {
+   "metric_id": "caseloads.disability_basic_income",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "disability_basic_income",
+   "label": "Disability basic income"
+  },
+  {
+   "metric_id": "caseloads.disability_basic_income",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "disability_basic_income",
+   "label": "Disability basic income"
+  },
+  {
+   "metric_id": "caseloads.disability_basic_income",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "disability_basic_income",
+   "label": "Disability basic income"
+  },
+  {
+   "metric_id": "caseloads.disability_basic_income",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "disability_basic_income",
+   "label": "Disability basic income"
+  },
+  {
+   "metric_id": "caseloads.disability_basic_income",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "disability_basic_income",
+   "label": "Disability basic income"
+  },
+  {
+   "metric_id": "caseloads.disability_basic_income",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "disability_basic_income",
+   "label": "Disability basic income"
+  },
+  {
+   "metric_id": "caseloads.disability_basic_income",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "caseload",
+   "partition": "disability_basic_income",
+   "label": "Disability basic income"
+  },
+  {
+   "metric_id": "spending.other_public_spending_budget_change",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "other_public_spending_budget_change",
+   "label": "Other public spending budget change"
+  },
+  {
+   "metric_id": "spending.other_public_spending_budget_change",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "other_public_spending_budget_change",
+   "label": "Other public spending budget change"
+  },
+  {
+   "metric_id": "spending.other_public_spending_budget_change",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "other_public_spending_budget_change",
+   "label": "Other public spending budget change"
+  },
+  {
+   "metric_id": "spending.other_public_spending_budget_change",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "other_public_spending_budget_change",
+   "label": "Other public spending budget change"
+  },
+  {
+   "metric_id": "spending.other_public_spending_budget_change",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "other_public_spending_budget_change",
+   "label": "Other public spending budget change"
+  },
+  {
+   "metric_id": "spending.other_public_spending_budget_change",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "other_public_spending_budget_change",
+   "label": "Other public spending budget change"
+  },
+  {
+   "metric_id": "spending.other_public_spending_budget_change",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "spending",
+   "partition": "other_public_spending_budget_change",
+   "label": "Other public spending budget change"
+  },
+  {
+   "metric_id": "double_counts.corporate_sdlt",
+   "year": 2025,
+   "value": 3130000159.912464,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "double_count",
+   "partition": null,
+   "label": "corporate_sdlt, counted again inside expected_sdlt"
+  },
+  {
+   "metric_id": "double_counts.corporate_sdlt",
+   "year": 2026,
+   "value": 3130000023.6063724,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "double_count",
+   "partition": null,
+   "label": "corporate_sdlt, counted again inside expected_sdlt"
+  },
+  {
+   "metric_id": "double_counts.corporate_sdlt",
+   "year": 2027,
+   "value": 3129999836.909186,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "double_count",
+   "partition": null,
+   "label": "corporate_sdlt, counted again inside expected_sdlt"
+  },
+  {
+   "metric_id": "double_counts.corporate_sdlt",
+   "year": 2028,
+   "value": 3129999962.584343,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "double_count",
+   "partition": null,
+   "label": "corporate_sdlt, counted again inside expected_sdlt"
+  },
+  {
+   "metric_id": "double_counts.corporate_sdlt",
+   "year": 2029,
+   "value": 3129999544.0565424,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "double_count",
+   "partition": null,
+   "label": "corporate_sdlt, counted again inside expected_sdlt"
+  },
+  {
+   "metric_id": "double_counts.corporate_sdlt",
+   "year": 2030,
+   "value": 3129999895.007397,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "double_count",
+   "partition": null,
+   "label": "corporate_sdlt, counted again inside expected_sdlt"
+  },
+  {
+   "metric_id": "double_counts.corporate_sdlt",
+   "year": 2031,
+   "value": 3129999484.877694,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "double_count",
+   "partition": null,
+   "label": "corporate_sdlt, counted again inside expected_sdlt"
+  },
+  {
+   "metric_id": "model.gov_tax",
+   "year": 2025,
+   "value": 969745776128.1699,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_tax"
+  },
+  {
+   "metric_id": "model.gov_tax",
+   "year": 2026,
+   "value": 1011784948712.7561,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_tax"
+  },
+  {
+   "metric_id": "model.gov_tax",
+   "year": 2027,
+   "value": 1050610465294.2213,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_tax"
+  },
+  {
+   "metric_id": "model.gov_tax",
+   "year": 2028,
+   "value": 1085614034747.1652,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_tax"
+  },
+  {
+   "metric_id": "model.gov_tax",
+   "year": 2029,
+   "value": 1124265122009.6616,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_tax"
+  },
+  {
+   "metric_id": "model.gov_tax",
+   "year": 2030,
+   "value": 1163453498626.6191,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_tax"
+  },
+  {
+   "metric_id": "model.gov_tax",
+   "year": 2031,
+   "value": 1206982869582.7024,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_tax"
+  },
+  {
+   "metric_id": "model.household_tax",
+   "year": 2025,
+   "value": 503977430092.485,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "household_tax"
+  },
+  {
+   "metric_id": "model.household_tax",
+   "year": 2026,
+   "value": 531055850973.52136,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "household_tax"
+  },
+  {
+   "metric_id": "model.household_tax",
+   "year": 2027,
+   "value": 557277774892.158,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "household_tax"
+  },
+  {
+   "metric_id": "model.household_tax",
+   "year": 2028,
+   "value": 579764609886.5848,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "household_tax"
+  },
+  {
+   "metric_id": "model.household_tax",
+   "year": 2029,
+   "value": 603325541929.8473,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "household_tax"
+  },
+  {
+   "metric_id": "model.household_tax",
+   "year": 2030,
+   "value": 628508152120.9532,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "household_tax"
+  },
+  {
+   "metric_id": "model.household_tax",
+   "year": 2031,
+   "value": 656263699478.5581,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "household_tax"
+  },
+  {
+   "metric_id": "model.pre_budget_change_household_tax",
+   "year": 2025,
+   "value": 496150814427.7457,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "pre_budget_change_household_tax"
+  },
+  {
+   "metric_id": "model.pre_budget_change_household_tax",
+   "year": 2026,
+   "value": 522932377454.86273,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "pre_budget_change_household_tax"
+  },
+  {
+   "metric_id": "model.pre_budget_change_household_tax",
+   "year": 2027,
+   "value": 548928559890.4607,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "pre_budget_change_household_tax"
+  },
+  {
+   "metric_id": "model.pre_budget_change_household_tax",
+   "year": 2028,
+   "value": 570722062751.3651,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "pre_budget_change_household_tax"
+  },
+  {
+   "metric_id": "model.pre_budget_change_household_tax",
+   "year": 2029,
+   "value": 594044368863.5272,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "pre_budget_change_household_tax"
+  },
+  {
+   "metric_id": "model.pre_budget_change_household_tax",
+   "year": 2030,
+   "value": 618963456079.3445,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "pre_budget_change_household_tax"
+  },
+  {
+   "metric_id": "model.pre_budget_change_household_tax",
+   "year": 2031,
+   "value": 646366448427.6294,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "pre_budget_change_household_tax"
+  },
+  {
+   "metric_id": "model.gov_spending",
+   "year": 2025,
+   "value": 536882494246.02844,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_spending"
+  },
+  {
+   "metric_id": "model.gov_spending",
+   "year": 2026,
+   "value": 551017143116.2849,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_spending"
+  },
+  {
+   "metric_id": "model.gov_spending",
+   "year": 2027,
+   "value": 557190195649.4889,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_spending"
+  },
+  {
+   "metric_id": "model.gov_spending",
+   "year": 2028,
+   "value": 564067532600.854,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_spending"
+  },
+  {
+   "metric_id": "model.gov_spending",
+   "year": 2029,
+   "value": 572364658929.4899,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_spending"
+  },
+  {
+   "metric_id": "model.gov_spending",
+   "year": 2030,
+   "value": 577022180786.2144,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_spending"
+  },
+  {
+   "metric_id": "model.gov_spending",
+   "year": 2031,
+   "value": 586109747489.8334,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_spending"
+  },
+  {
+   "metric_id": "model.household_benefits",
+   "year": 2025,
+   "value": 536882494246.02844,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "household_benefits"
+  },
+  {
+   "metric_id": "model.household_benefits",
+   "year": 2026,
+   "value": 551017143116.2849,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "household_benefits"
+  },
+  {
+   "metric_id": "model.household_benefits",
+   "year": 2027,
+   "value": 557190195649.4889,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "household_benefits"
+  },
+  {
+   "metric_id": "model.household_benefits",
+   "year": 2028,
+   "value": 564067532600.854,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "household_benefits"
+  },
+  {
+   "metric_id": "model.household_benefits",
+   "year": 2029,
+   "value": 572364658929.4899,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "household_benefits"
+  },
+  {
+   "metric_id": "model.household_benefits",
+   "year": 2030,
+   "value": 577022180786.2144,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "household_benefits"
+  },
+  {
+   "metric_id": "model.household_benefits",
+   "year": 2031,
+   "value": 586109747489.8334,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "household_benefits"
+  },
+  {
+   "metric_id": "balance.receipts",
+   "year": 2025,
+   "value": 966615777128.9729,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government receipts, each component once"
+  },
+  {
+   "metric_id": "balance.receipts",
+   "year": 2026,
+   "value": 1008654948197.3245,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government receipts, each component once"
+  },
+  {
+   "metric_id": "balance.receipts",
+   "year": 2027,
+   "value": 1047480466355.038,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government receipts, each component once"
+  },
+  {
+   "metric_id": "balance.receipts",
+   "year": 2028,
+   "value": 1082484034898.3741,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government receipts, each component once"
+  },
+  {
+   "metric_id": "balance.receipts",
+   "year": 2029,
+   "value": 1121135120971.1062,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government receipts, each component once"
+  },
+  {
+   "metric_id": "balance.receipts",
+   "year": 2030,
+   "value": 1160323499762.114,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government receipts, each component once"
+  },
+  {
+   "metric_id": "balance.receipts",
+   "year": 2031,
+   "value": 1203852872416.6934,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government receipts, each component once"
+  },
+  {
+   "metric_id": "balance.spending",
+   "year": 2025,
+   "value": 536882494246.02844,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government spending"
+  },
+  {
+   "metric_id": "balance.spending",
+   "year": 2026,
+   "value": 551017143116.2849,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government spending"
+  },
+  {
+   "metric_id": "balance.spending",
+   "year": 2027,
+   "value": 557190195649.4889,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government spending"
+  },
+  {
+   "metric_id": "balance.spending",
+   "year": 2028,
+   "value": 564067532600.854,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government spending"
+  },
+  {
+   "metric_id": "balance.spending",
+   "year": 2029,
+   "value": 572364658929.4899,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government spending"
+  },
+  {
+   "metric_id": "balance.spending",
+   "year": 2030,
+   "value": 577022180786.2144,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government spending"
+  },
+  {
+   "metric_id": "balance.spending",
+   "year": 2031,
+   "value": 586109747489.8334,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government spending"
+  },
+  {
+   "metric_id": "balance.balance",
+   "year": 2025,
+   "value": 429733282882.94446,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": -1
+   },
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government balance, each receipt once"
+  },
+  {
+   "metric_id": "balance.balance",
+   "year": 2026,
+   "value": 457637805081.0396,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": -1
+   },
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government balance, each receipt once"
+  },
+  {
+   "metric_id": "balance.balance",
+   "year": 2027,
+   "value": 490290270705.5491,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": -1
+   },
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government balance, each receipt once"
+  },
+  {
+   "metric_id": "balance.balance",
+   "year": 2028,
+   "value": 518416502297.52026,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": -1
+   },
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government balance, each receipt once"
+  },
+  {
+   "metric_id": "balance.balance",
+   "year": 2029,
+   "value": 548770462041.61646,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": -1
+   },
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government balance, each receipt once"
+  },
+  {
+   "metric_id": "balance.balance",
+   "year": 2030,
+   "value": 583301318975.8997,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": -1
+   },
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government balance, each receipt once"
+  },
+  {
+   "metric_id": "balance.balance",
+   "year": 2031,
+   "value": 617743124926.8599,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": -1
+   },
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Government balance, each receipt once"
+  },
+  {
+   "metric_id": "balance.household_only_benefits",
+   "year": 2025,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Benefits in household_benefits but not in gov_spending"
+  },
+  {
+   "metric_id": "balance.household_only_benefits",
+   "year": 2026,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Benefits in household_benefits but not in gov_spending"
+  },
+  {
+   "metric_id": "balance.household_only_benefits",
+   "year": 2027,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Benefits in household_benefits but not in gov_spending"
+  },
+  {
+   "metric_id": "balance.household_only_benefits",
+   "year": 2028,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Benefits in household_benefits but not in gov_spending"
+  },
+  {
+   "metric_id": "balance.household_only_benefits",
+   "year": 2029,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Benefits in household_benefits but not in gov_spending"
+  },
+  {
+   "metric_id": "balance.household_only_benefits",
+   "year": 2030,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Benefits in household_benefits but not in gov_spending"
+  },
+  {
+   "metric_id": "balance.household_only_benefits",
+   "year": 2031,
+   "value": 0.0,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "balance",
+   "partition": null,
+   "label": "Benefits in household_benefits but not in gov_spending"
+  },
+  {
+   "metric_id": "model.gov_balance",
+   "year": 2025,
+   "value": 432863283042.85693,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": -1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_balance"
+  },
+  {
+   "metric_id": "model.gov_balance",
+   "year": 2026,
+   "value": 460767805104.64594,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": -1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_balance"
+  },
+  {
+   "metric_id": "model.gov_balance",
+   "year": 2027,
+   "value": 493420270542.4583,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": -1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_balance"
+  },
+  {
+   "metric_id": "model.gov_balance",
+   "year": 2028,
+   "value": 521546502260.1046,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": -1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_balance"
+  },
+  {
+   "metric_id": "model.gov_balance",
+   "year": 2029,
+   "value": 551900461585.6729,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": -1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_balance"
+  },
+  {
+   "metric_id": "model.gov_balance",
+   "year": 2030,
+   "value": 586431318870.907,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": -1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_balance"
+  },
+  {
+   "metric_id": "model.gov_balance",
+   "year": 2031,
+   "value": 620873124411.7375,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": -1
+   },
+   "unit": "gbp",
+   "kind": "model_total",
+   "partition": null,
+   "label": "gov_balance"
+  },
+  {
+   "metric_id": "household.household_market_income",
+   "year": 2025,
+   "value": 1657055494245.5037,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household market income"
+  },
+  {
+   "metric_id": "household.household_market_income",
+   "year": 2026,
+   "value": 1719245701089.3281,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household market income"
+  },
+  {
+   "metric_id": "household.household_market_income",
+   "year": 2027,
+   "value": 1771683231648.2764,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household market income"
+  },
+  {
+   "metric_id": "household.household_market_income",
+   "year": 2028,
+   "value": 1821985672731.2063,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household market income"
+  },
+  {
+   "metric_id": "household.household_market_income",
+   "year": 2029,
+   "value": 1887412879505.8608,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household market income"
+  },
+  {
+   "metric_id": "household.household_market_income",
+   "year": 2030,
+   "value": 1945587770944.968,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household market income"
+  },
+  {
+   "metric_id": "household.household_market_income",
+   "year": 2031,
+   "value": 2017368706417.267,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household market income"
+  },
+  {
+   "metric_id": "household.household_net_income",
+   "year": 2025,
+   "value": 1650695092699.1787,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household net income"
+  },
+  {
+   "metric_id": "household.household_net_income",
+   "year": 2026,
+   "value": 1698452218971.148,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household net income"
+  },
+  {
+   "metric_id": "household.household_net_income",
+   "year": 2027,
+   "value": 1729708349505.7048,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household net income"
+  },
+  {
+   "metric_id": "household.household_net_income",
+   "year": 2028,
+   "value": 1763350595681.0042,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household net income"
+  },
+  {
+   "metric_id": "household.household_net_income",
+   "year": 2029,
+   "value": 1797840584674.6392,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household net income"
+  },
+  {
+   "metric_id": "household.household_net_income",
+   "year": 2030,
+   "value": 1834164055321.444,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household net income"
+  },
+  {
+   "metric_id": "household.household_net_income",
+   "year": 2031,
+   "value": 1884778958157.876,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household net income"
+  },
+  {
+   "metric_id": "household.hbai_household_net_income",
+   "year": 2025,
+   "value": 1468438454639.119,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household net income (HBAI definition)"
+  },
+  {
+   "metric_id": "household.hbai_household_net_income",
+   "year": 2026,
+   "value": 1515139365275.6355,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household net income (HBAI definition)"
+  },
+  {
+   "metric_id": "household.hbai_household_net_income",
+   "year": 2027,
+   "value": 1547358223256.206,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household net income (HBAI definition)"
+  },
+  {
+   "metric_id": "household.hbai_household_net_income",
+   "year": 2028,
+   "value": 1580410096999.486,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household net income (HBAI definition)"
+  },
+  {
+   "metric_id": "household.hbai_household_net_income",
+   "year": 2029,
+   "value": 1613429021347.468,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household net income (HBAI definition)"
+  },
+  {
+   "metric_id": "household.hbai_household_net_income",
+   "year": 2030,
+   "value": 1647838190515.2617,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household net income (HBAI definition)"
+  },
+  {
+   "metric_id": "household.hbai_household_net_income",
+   "year": 2031,
+   "value": 1697900004504.373,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "household_total",
+   "partition": null,
+   "label": "Household net income (HBAI definition)"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.all",
+   "year": 2025,
+   "value": 0.158961068989031,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.all",
+   "year": 2026,
+   "value": 0.149417048095037,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.all",
+   "year": 2027,
+   "value": 0.1551359605887892,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.all",
+   "year": 2028,
+   "value": 0.15590985583220196,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.all",
+   "year": 2029,
+   "value": 0.15717923007921353,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.all",
+   "year": 2030,
+   "value": 0.1610142002033216,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.all",
+   "year": 2031,
+   "value": 0.1599545027297433,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.all",
+   "year": 2025,
+   "value": 11050012.62085329,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.all",
+   "year": 2026,
+   "value": 10426039.302517518,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.all",
+   "year": 2027,
+   "value": 10865147.24560164,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.all",
+   "year": 2028,
+   "value": 10963024.831684813,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.all",
+   "year": 2029,
+   "value": 11100912.898740932,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.all",
+   "year": 2030,
+   "value": 11422934.10709335,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.all",
+   "year": 2031,
+   "value": 11396550.62013276,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.children",
+   "year": 2025,
+   "value": 0.2372510378251411,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.children",
+   "year": 2026,
+   "value": 0.20852661548421755,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.children",
+   "year": 2027,
+   "value": 0.21671681039197135,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.children",
+   "year": 2028,
+   "value": 0.21457714000706526,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.children",
+   "year": 2029,
+   "value": 0.21642597560772944,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.children",
+   "year": 2030,
+   "value": 0.2217104975392767,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.children",
+   "year": 2031,
+   "value": 0.22078118293404653,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.children",
+   "year": 2025,
+   "value": 3559375.146835327,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.children",
+   "year": 2026,
+   "value": 3140323.1707639247,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.children",
+   "year": 2027,
+   "value": 3275739.6749524027,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.children",
+   "year": 2028,
+   "value": 3256371.3432342857,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.children",
+   "year": 2029,
+   "value": 3298880.366600305,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.children",
+   "year": 2030,
+   "value": 3394637.414582446,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.children",
+   "year": 2031,
+   "value": 3394944.311651394,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.working_age_adults",
+   "year": 2025,
+   "value": 0.13767748825105397,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.working_age_adults",
+   "year": 2026,
+   "value": 0.13372106552488075,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.working_age_adults",
+   "year": 2027,
+   "value": 0.13989204520959575,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.working_age_adults",
+   "year": 2028,
+   "value": 0.14124164361087443,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.working_age_adults",
+   "year": 2029,
+   "value": 0.14248707378049913,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.working_age_adults",
+   "year": 2030,
+   "value": 0.14658630153368837,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.working_age_adults",
+   "year": 2031,
+   "value": 0.14525417694146392,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.working_age_adults",
+   "year": 2025,
+   "value": 5863952.6221417785,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.working_age_adults",
+   "year": 2026,
+   "value": 5736660.92677407,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.working_age_adults",
+   "year": 2027,
+   "value": 6072487.460049972,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.working_age_adults",
+   "year": 2028,
+   "value": 6178484.984416798,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.working_age_adults",
+   "year": 2029,
+   "value": 6260390.306578115,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.working_age_adults",
+   "year": 2030,
+   "value": 6469478.6622083485,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.working_age_adults",
+   "year": 2031,
+   "value": 6438252.252475888,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.pensioners",
+   "year": 2025,
+   "value": 0.13647330334302774,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.pensioners",
+   "year": 2026,
+   "value": 0.13107228723131284,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.pensioners",
+   "year": 2027,
+   "value": 0.13176164489560352,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.pensioners",
+   "year": 2028,
+   "value": 0.13408989504529978,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.pensioners",
+   "year": 2029,
+   "value": 0.13467956142673865,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.pensioners",
+   "year": 2030,
+   "value": 0.13556998964969896,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.absolute_bhc.pensioners",
+   "year": 2031,
+   "value": 0.13538234298373342,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.pensioners",
+   "year": 2025,
+   "value": 1626684.8518761843,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.pensioners",
+   "year": 2026,
+   "value": 1549055.204979524,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.pensioners",
+   "year": 2027,
+   "value": 1516920.1105992645,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.pensioners",
+   "year": 2028,
+   "value": 1528168.5040337294,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.pensioners",
+   "year": 2029,
+   "value": 1541642.2255625129,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.pensioners",
+   "year": 2030,
+   "value": 1558818.0303025544,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.absolute_bhc.pensioners",
+   "year": 2031,
+   "value": 1563354.056005478,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.all",
+   "year": 2025,
+   "value": 0.20913249144654364,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.all",
+   "year": 2026,
+   "value": 0.20368321654160576,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.all",
+   "year": 2027,
+   "value": 0.2099396779334123,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.all",
+   "year": 2028,
+   "value": 0.21265695145642433,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.all",
+   "year": 2029,
+   "value": 0.2154006487190125,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.all",
+   "year": 2030,
+   "value": 0.22356054596434546,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.all",
+   "year": 2031,
+   "value": 0.223770468364186,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.all",
+   "year": 2025,
+   "value": 14537626.631551296,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.all",
+   "year": 2026,
+   "value": 14212630.00441049,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.all",
+   "year": 2027,
+   "value": 14703396.329152241,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.all",
+   "year": 2028,
+   "value": 14953278.142699987,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.all",
+   "year": 2029,
+   "value": 15212848.660455883,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.all",
+   "year": 2030,
+   "value": 15860199.797730953,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.all",
+   "year": 2031,
+   "value": 15943355.307177946,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.children",
+   "year": 2025,
+   "value": 0.3176647822027365,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.children",
+   "year": 2026,
+   "value": 0.29976792277040976,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.children",
+   "year": 2027,
+   "value": 0.3075246154957757,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.children",
+   "year": 2028,
+   "value": 0.31040394928891696,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.children",
+   "year": 2029,
+   "value": 0.31404388184177656,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.children",
+   "year": 2030,
+   "value": 0.32925032071219956,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.children",
+   "year": 2031,
+   "value": 0.3295364201437651,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.children",
+   "year": 2025,
+   "value": 4765787.92304638,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.children",
+   "year": 2026,
+   "value": 4514378.90334405,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.children",
+   "year": 2027,
+   "value": 4648326.921118781,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.children",
+   "year": 2028,
+   "value": 4710616.076148167,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.children",
+   "year": 2029,
+   "value": 4786824.655172229,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.children",
+   "year": 2030,
+   "value": 5041193.222052559,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.children",
+   "year": 2031,
+   "value": 5067269.683862701,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.working_age_adults",
+   "year": 2025,
+   "value": 0.19431403398519395,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.working_age_adults",
+   "year": 2026,
+   "value": 0.19237286661348602,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.working_age_adults",
+   "year": 2027,
+   "value": 0.1997992793069061,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.working_age_adults",
+   "year": 2028,
+   "value": 0.20238185535311953,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.working_age_adults",
+   "year": 2029,
+   "value": 0.20530273277647584,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.working_age_adults",
+   "year": 2030,
+   "value": 0.2131128866367783,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.working_age_adults",
+   "year": 2031,
+   "value": 0.21322897588420822,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.working_age_adults",
+   "year": 2025,
+   "value": 8276213.515957296,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.working_age_adults",
+   "year": 2026,
+   "value": 8252835.130660608,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.working_age_adults",
+   "year": 2027,
+   "value": 8672963.61491029,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.working_age_adults",
+   "year": 2028,
+   "value": 8853006.963460386,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.working_age_adults",
+   "year": 2029,
+   "value": 9020293.589352593,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.working_age_adults",
+   "year": 2030,
+   "value": 9405580.591863185,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.working_age_adults",
+   "year": 2031,
+   "value": 9451170.101861268,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.pensioners",
+   "year": 2025,
+   "value": 0.12547784554248842,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.pensioners",
+   "year": 2026,
+   "value": 0.12230292156970884,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.pensioners",
+   "year": 2027,
+   "value": 0.12005149872375881,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.pensioners",
+   "year": 2028,
+   "value": 0.12193596873043704,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.pensioners",
+   "year": 2029,
+   "value": 0.12280615616424372,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.pensioners",
+   "year": 2030,
+   "value": 0.1229252820223357,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.absolute_ahc.pensioners",
+   "year": 2031,
+   "value": 0.12339393057336441,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.pensioners",
+   "year": 2025,
+   "value": 1495625.1925476193,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.pensioners",
+   "year": 2026,
+   "value": 1445415.970405832,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.pensioners",
+   "year": 2027,
+   "value": 1382105.7931231707,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.pensioners",
+   "year": 2028,
+   "value": 1389655.1030914336,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.pensioners",
+   "year": 2029,
+   "value": 1405730.415931061,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.pensioners",
+   "year": 2030,
+   "value": 1413425.983815208,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.absolute_ahc.pensioners",
+   "year": 2031,
+   "value": 1424915.5214539766,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Absolute poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.all",
+   "year": 2025,
+   "value": 0.16996315965340555,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.all",
+   "year": 2026,
+   "value": 0.1641757039106901,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.all",
+   "year": 2027,
+   "value": 0.1658306356282735,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.all",
+   "year": 2028,
+   "value": 0.16675706960047298,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.all",
+   "year": 2029,
+   "value": 0.16818506457390087,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.all",
+   "year": 2030,
+   "value": 0.16603686894151898,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.all",
+   "year": 2031,
+   "value": 0.16939429823668872,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.all",
+   "year": 2025,
+   "value": 11814811.457891181,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.all",
+   "year": 2026,
+   "value": 11455870.419837251,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.all",
+   "year": 2027,
+   "value": 11614162.616421178,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.all",
+   "year": 2028,
+   "value": 11725762.204902157,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.all",
+   "year": 2029,
+   "value": 11878209.046851024,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.all",
+   "year": 2030,
+   "value": 11779260.530264333,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.all",
+   "year": 2031,
+   "value": 12069123.79252024,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.children",
+   "year": 2025,
+   "value": 0.2576548985094404,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.children",
+   "year": 2026,
+   "value": 0.2349385317072657,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.children",
+   "year": 2027,
+   "value": 0.23473844147435505,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.children",
+   "year": 2028,
+   "value": 0.23360704801218143,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.children",
+   "year": 2029,
+   "value": 0.23495351453647773,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.children",
+   "year": 2030,
+   "value": 0.23174222961978033,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.children",
+   "year": 2031,
+   "value": 0.23914597293246234,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.children",
+   "year": 2025,
+   "value": 3865485.4816306233,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.children",
+   "year": 2026,
+   "value": 3538075.5262937695,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.children",
+   "year": 2027,
+   "value": 3548142.041142389,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.children",
+   "year": 2028,
+   "value": 3545164.6745751947,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.children",
+   "year": 2029,
+   "value": 3581287.015071422,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.children",
+   "year": 2030,
+   "value": 3548234.530783549,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.children",
+   "year": 2031,
+   "value": 3677339.0271395296,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: children"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.working_age_adults",
+   "year": 2025,
+   "value": 0.14512498963363443,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.working_age_adults",
+   "year": 2026,
+   "value": 0.14359366405754964,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.working_age_adults",
+   "year": 2027,
+   "value": 0.1475713897931732,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.working_age_adults",
+   "year": 2028,
+   "value": 0.1494918942688663,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.working_age_adults",
+   "year": 2029,
+   "value": 0.1516084128453753,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.working_age_adults",
+   "year": 2030,
+   "value": 0.1503840561165627,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.working_age_adults",
+   "year": 2031,
+   "value": 0.15254973464975796,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.working_age_adults",
+   "year": 2025,
+   "value": 6181156.224673748,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.working_age_adults",
+   "year": 2026,
+   "value": 6160197.413159236,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.working_age_adults",
+   "year": 2027,
+   "value": 6405835.3899862915,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.working_age_adults",
+   "year": 2028,
+   "value": 6539384.56406565,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.working_age_adults",
+   "year": 2029,
+   "value": 6661150.467831269,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.working_age_adults",
+   "year": 2030,
+   "value": 6637089.76898399,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.working_age_adults",
+   "year": 2031,
+   "value": 6761620.8594070375,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.pensioners",
+   "year": 2025,
+   "value": 0.14834340320557673,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.pensioners",
+   "year": 2026,
+   "value": 0.14871795468967855,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.pensioners",
+   "year": 2027,
+   "value": 0.14420583478126237,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.pensioners",
+   "year": 2028,
+   "value": 0.14400903683858882,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.pensioners",
+   "year": 2029,
+   "value": 0.14290280401894626,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.pensioners",
+   "year": 2030,
+   "value": 0.1386242101836538,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.relative_bhc.pensioners",
+   "year": 2031,
+   "value": 0.14116789999709253,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.pensioners",
+   "year": 2025,
+   "value": 1768169.7515868098,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.pensioners",
+   "year": 2026,
+   "value": 1757597.4803842455,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.pensioners",
+   "year": 2027,
+   "value": 1660185.1852924973,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.pensioners",
+   "year": 2028,
+   "value": 1641212.9662613124,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.pensioners",
+   "year": 2029,
+   "value": 1635771.5639483333,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.pensioners",
+   "year": 2030,
+   "value": 1593936.230496794,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.relative_bhc.pensioners",
+   "year": 2031,
+   "value": 1630163.9059736729,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, before housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.all",
+   "year": 2025,
+   "value": 0.21456512996755925,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.all",
+   "year": 2026,
+   "value": 0.2108568153025047,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.all",
+   "year": 2027,
+   "value": 0.21280995156860974,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.all",
+   "year": 2028,
+   "value": 0.2141820445617756,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.all",
+   "year": 2029,
+   "value": 0.2155949209344568,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.all",
+   "year": 2030,
+   "value": 0.21996997492343381,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.all",
+   "year": 2031,
+   "value": 0.2227237561956704,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.all",
+   "year": 2025,
+   "value": 14915270.822066262,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.all",
+   "year": 2026,
+   "value": 14713190.171909258,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.all",
+   "year": 2027,
+   "value": 14904419.64807345,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.all",
+   "year": 2028,
+   "value": 15060517.248883188,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.all",
+   "year": 2029,
+   "value": 15226569.296071708,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.all",
+   "year": 2030,
+   "value": 15605471.6038488,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.all",
+   "year": 2031,
+   "value": 15868778.424316749,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: everyone"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.children",
+   "year": 2025,
+   "value": 0.32573092950864546,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.children",
+   "year": 2026,
+   "value": 0.3129215499802995,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.children",
+   "year": 2027,
+   "value": 0.31080197766773193,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.children",
+   "year": 2028,
+   "value": 0.3112443481181635,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.children",
+   "year": 2029,
+   "value": 0.31435135617461696,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.children",
+   "year": 2030,
+   "value": 0.32341397660487087,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.children",
+   "year": 2031,
+   "value": 0.32841073439755847,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.children",
+   "year": 2025,
+   "value": 4886800.85733974,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.children",
+   "year": 2026,
+   "value": 4712466.999728724,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.children",
+   "year": 2027,
+   "value": 4697865.234627768,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.children",
+   "year": 2028,
+   "value": 4723369.767731324,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.children",
+   "year": 2029,
+   "value": 4791511.343251109,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.children",
+   "year": 2030,
+   "value": 4951832.22069715,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.children",
+   "year": 2031,
+   "value": 5049960.054618016,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: children"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.working_age_adults",
+   "year": 2025,
+   "value": 0.19817681755370278,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.working_age_adults",
+   "year": 2026,
+   "value": 0.19800752646020686,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.working_age_adults",
+   "year": 2027,
+   "value": 0.2018228258946827,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.working_age_adults",
+   "year": 2028,
+   "value": 0.20314734474442595,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.working_age_adults",
+   "year": 2029,
+   "value": 0.2054474903629614,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.working_age_adults",
+   "year": 2030,
+   "value": 0.21027795561183132,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.working_age_adults",
+   "year": 2031,
+   "value": 0.2126635701601802,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.working_age_adults",
+   "year": 2025,
+   "value": 8440736.998503834,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.working_age_adults",
+   "year": 2026,
+   "value": 8494563.18488653,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.working_age_adults",
+   "year": 2027,
+   "value": 8760802.499964043,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.working_age_adults",
+   "year": 2028,
+   "value": 8886492.58844325,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.working_age_adults",
+   "year": 2029,
+   "value": 9026653.738152012,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.working_age_adults",
+   "year": 2030,
+   "value": 9280462.995042503,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.working_age_adults",
+   "year": 2031,
+   "value": 9426109.034751624,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: working-age adults"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.pensioners",
+   "year": 2025,
+   "value": 0.133205373171771,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.pensioners",
+   "year": 2026,
+   "value": 0.12744273660248256,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.pensioners",
+   "year": 2027,
+   "value": 0.125579883146286,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.pensioners",
+   "year": 2028,
+   "value": 0.12728842519447406,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.pensioners",
+   "year": 2029,
+   "value": 0.12303974216451169,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.pensioners",
+   "year": 2030,
+   "value": 0.119424785385011,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty.relative_ahc.pensioners",
+   "year": 2031,
+   "value": 0.12060495966103543,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "rate",
+   "kind": "poverty",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.pensioners",
+   "year": 2025,
+   "value": 1587732.9662226886,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.pensioners",
+   "year": 2026,
+   "value": 1506159.9872940034,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.pensioners",
+   "year": 2027,
+   "value": 1445751.9134816378,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.pensioners",
+   "year": 2028,
+   "value": 1450654.8927086145,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.pensioners",
+   "year": 2029,
+   "value": 1408404.2146685869,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.pensioners",
+   "year": 2030,
+   "value": 1373176.3881091475,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "poverty_count.relative_ahc.pensioners",
+   "year": 2031,
+   "value": 1392709.3349471092,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "count",
+   "kind": "poverty_count",
+   "partition": null,
+   "label": "Relative poverty, after housing costs: pensioners"
+  },
+  {
+   "metric_id": "inequality.gini_equiv_hbai_bhc",
+   "year": 2025,
+   "value": 0.3077487914075191,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "index",
+   "kind": "inequality",
+   "partition": null,
+   "label": "Gini coefficient, equivalised HBAI income before housing costs"
+  },
+  {
+   "metric_id": "inequality.gini_equiv_hbai_bhc",
+   "year": 2026,
+   "value": 0.3054631654461407,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "index",
+   "kind": "inequality",
+   "partition": null,
+   "label": "Gini coefficient, equivalised HBAI income before housing costs"
+  },
+  {
+   "metric_id": "inequality.gini_equiv_hbai_bhc",
+   "year": 2027,
+   "value": 0.3069197698442653,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "index",
+   "kind": "inequality",
+   "partition": null,
+   "label": "Gini coefficient, equivalised HBAI income before housing costs"
+  },
+  {
+   "metric_id": "inequality.gini_equiv_hbai_bhc",
+   "year": 2028,
+   "value": 0.3077757166797609,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "index",
+   "kind": "inequality",
+   "partition": null,
+   "label": "Gini coefficient, equivalised HBAI income before housing costs"
+  },
+  {
+   "metric_id": "inequality.gini_equiv_hbai_bhc",
+   "year": 2029,
+   "value": 0.3082329014779346,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "index",
+   "kind": "inequality",
+   "partition": null,
+   "label": "Gini coefficient, equivalised HBAI income before housing costs"
+  },
+  {
+   "metric_id": "inequality.gini_equiv_hbai_bhc",
+   "year": 2030,
+   "value": 0.3099891970371004,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "index",
+   "kind": "inequality",
+   "partition": null,
+   "label": "Gini coefficient, equivalised HBAI income before housing costs"
+  },
+  {
+   "metric_id": "inequality.gini_equiv_hbai_bhc",
+   "year": 2031,
+   "value": 0.3110144570254967,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "index",
+   "kind": "inequality",
+   "partition": null,
+   "label": "Gini coefficient, equivalised HBAI income before housing costs"
+  },
+  {
+   "metric_id": "deciles.mean_income.1",
+   "year": 2025,
+   "value": 17676.754667048575,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 1: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.1",
+   "year": 2026,
+   "value": 17959.072766481713,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 1: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.1",
+   "year": 2027,
+   "value": 17890.694840247495,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 1: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.1",
+   "year": 2028,
+   "value": 17941.1511777277,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 1: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.1",
+   "year": 2029,
+   "value": 18267.238369432558,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 1: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.1",
+   "year": 2030,
+   "value": 18350.30992490091,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 1: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.1",
+   "year": 2031,
+   "value": 18550.46356681594,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 1: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.2",
+   "year": 2025,
+   "value": 28179.055803438234,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 2: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.2",
+   "year": 2026,
+   "value": 28837.04533117914,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 2: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.2",
+   "year": 2027,
+   "value": 28424.564554846565,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 2: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.2",
+   "year": 2028,
+   "value": 28883.45566735774,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 2: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.2",
+   "year": 2029,
+   "value": 29061.95653619478,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 2: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.2",
+   "year": 2030,
+   "value": 29759.483201387062,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 2: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.2",
+   "year": 2031,
+   "value": 30221.58748982937,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 2: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.3",
+   "year": 2025,
+   "value": 34530.61843120579,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 3: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.3",
+   "year": 2026,
+   "value": 35774.3120618477,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 3: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.3",
+   "year": 2027,
+   "value": 36424.80493358469,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 3: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.3",
+   "year": 2028,
+   "value": 36772.53572338885,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 3: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.3",
+   "year": 2029,
+   "value": 37156.5637359853,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 3: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.3",
+   "year": 2030,
+   "value": 36685.78880542181,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 3: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.3",
+   "year": 2031,
+   "value": 37377.12158903903,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 3: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.4",
+   "year": 2025,
+   "value": 38934.091210418585,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 4: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.4",
+   "year": 2026,
+   "value": 40539.44017480899,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 4: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.4",
+   "year": 2027,
+   "value": 41468.195696983545,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 4: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.4",
+   "year": 2028,
+   "value": 42373.92850208078,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 4: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.4",
+   "year": 2029,
+   "value": 42841.52793419517,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 4: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.4",
+   "year": 2030,
+   "value": 43336.4971038302,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 4: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.4",
+   "year": 2031,
+   "value": 44285.75677905547,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 4: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.5",
+   "year": 2025,
+   "value": 44653.353783363804,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 5: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.5",
+   "year": 2026,
+   "value": 46131.613207371694,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 5: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.5",
+   "year": 2027,
+   "value": 47073.502879079664,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 5: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.5",
+   "year": 2028,
+   "value": 47372.95055950299,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 5: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.5",
+   "year": 2029,
+   "value": 48085.54776182688,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 5: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.5",
+   "year": 2030,
+   "value": 48110.89818838793,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 5: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.5",
+   "year": 2031,
+   "value": 48857.4314782977,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 5: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.6",
+   "year": 2025,
+   "value": 48283.828312490165,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 6: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.6",
+   "year": 2026,
+   "value": 49405.72352548862,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 6: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.6",
+   "year": 2027,
+   "value": 50162.973103580414,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 6: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.6",
+   "year": 2028,
+   "value": 51093.240160099325,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 6: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.6",
+   "year": 2029,
+   "value": 52375.72388322683,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 6: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.6",
+   "year": 2030,
+   "value": 53906.52504776521,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 6: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.6",
+   "year": 2031,
+   "value": 55326.758982061125,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 6: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.7",
+   "year": 2025,
+   "value": 54559.719261696424,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 7: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.7",
+   "year": 2026,
+   "value": 55738.7030110715,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 7: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.7",
+   "year": 2027,
+   "value": 56769.742467299,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 7: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.7",
+   "year": 2028,
+   "value": 57552.98337993226,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 7: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.7",
+   "year": 2029,
+   "value": 58018.55725963795,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 7: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.7",
+   "year": 2030,
+   "value": 59147.87210110048,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 7: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.7",
+   "year": 2031,
+   "value": 60637.60697260916,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 7: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.8",
+   "year": 2025,
+   "value": 69236.41966938555,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 8: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.8",
+   "year": 2026,
+   "value": 70232.37813773785,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 8: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.8",
+   "year": 2027,
+   "value": 71531.33103123217,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 8: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.8",
+   "year": 2028,
+   "value": 72791.02280987396,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 8: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.8",
+   "year": 2029,
+   "value": 74106.60242542511,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 8: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.8",
+   "year": 2030,
+   "value": 75628.31856190029,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 8: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.8",
+   "year": 2031,
+   "value": 77611.71088167054,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 8: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.9",
+   "year": 2025,
+   "value": 79276.26633204655,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 9: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.9",
+   "year": 2026,
+   "value": 81581.90782711905,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 9: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.9",
+   "year": 2027,
+   "value": 83106.87038322185,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 9: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.9",
+   "year": 2028,
+   "value": 84575.49840158923,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 9: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.9",
+   "year": 2029,
+   "value": 85864.95896332423,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 9: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.9",
+   "year": 2030,
+   "value": 87649.69567899546,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 9: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.9",
+   "year": 2031,
+   "value": 90654.08333059541,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 9: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.10",
+   "year": 2025,
+   "value": 118030.04118236576,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 10: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.10",
+   "year": 2026,
+   "value": 120826.03138084599,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 10: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.10",
+   "year": 2027,
+   "value": 122592.51237304772,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 10: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.10",
+   "year": 2028,
+   "value": 124339.6078872293,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 10: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.10",
+   "year": 2029,
+   "value": 126219.29213328264,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 10: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.10",
+   "year": 2030,
+   "value": 128486.66905357645,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 10: mean household_net_income per household"
+  },
+  {
+   "metric_id": "deciles.mean_income.10",
+   "year": 2031,
+   "value": 131631.3290311054,
+   "suppressed": false,
+   "excludes": [
+    "spending.disabled_students_allowance"
+   ],
+   "exclude_signs": {
+    "spending.disabled_students_allowance": 1
+   },
+   "unit": "gbp_household",
+   "kind": "decile",
+   "partition": null,
+   "label": "Decile 10: mean household_net_income per household"
+  },
+  {
+   "metric_id": "reforms.basic_rate_plus_1pp",
+   "year": 2025,
+   "value": 6222687308.254578,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise basic rate by 1pp"
+  },
+  {
+   "metric_id": "reforms.basic_rate_plus_1pp",
+   "year": 2026,
+   "value": 6508759614.741272,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise basic rate by 1pp"
+  },
+  {
+   "metric_id": "reforms.basic_rate_plus_1pp",
+   "year": 2027,
+   "value": 6714986157.079834,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise basic rate by 1pp"
+  },
+  {
+   "metric_id": "reforms.basic_rate_plus_1pp",
+   "year": 2028,
+   "value": 6913272334.154663,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise basic rate by 1pp"
+  },
+  {
+   "metric_id": "reforms.basic_rate_plus_1pp",
+   "year": 2029,
+   "value": 7118288039.173706,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise basic rate by 1pp"
+  },
+  {
+   "metric_id": "reforms.basic_rate_plus_1pp",
+   "year": 2030,
+   "value": 7356266082.489136,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise basic rate by 1pp"
+  },
+  {
+   "metric_id": "reforms.basic_rate_plus_1pp",
+   "year": 2031,
+   "value": 7633159472.480713,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise basic rate by 1pp"
+  },
+  {
+   "metric_id": "reform_targets.basic_rate_plus_1pp",
+   "year": 2025,
+   "value": 6366582048.682007,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise basic rate by 1pp: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.basic_rate_plus_1pp",
+   "year": 2026,
+   "value": 6660786957.415405,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise basic rate by 1pp: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.basic_rate_plus_1pp",
+   "year": 2027,
+   "value": 6876392706.334412,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise basic rate by 1pp: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.basic_rate_plus_1pp",
+   "year": 2028,
+   "value": 7081815371.239807,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise basic rate by 1pp: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.basic_rate_plus_1pp",
+   "year": 2029,
+   "value": 7290951197.121887,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise basic rate by 1pp: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.basic_rate_plus_1pp",
+   "year": 2030,
+   "value": 7531692335.065491,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise basic rate by 1pp: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.basic_rate_plus_1pp",
+   "year": 2031,
+   "value": 7809724834.753845,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise basic rate by 1pp: change in income_tax"
+  },
+  {
+   "metric_id": "reforms.higher_rate_plus_1pp",
+   "year": 2025,
+   "value": 3548582094.3916626,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise higher rate by 1pp"
+  },
+  {
+   "metric_id": "reforms.higher_rate_plus_1pp",
+   "year": 2026,
+   "value": 3840615655.607727,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise higher rate by 1pp"
+  },
+  {
+   "metric_id": "reforms.higher_rate_plus_1pp",
+   "year": 2027,
+   "value": 4076238853.5632935,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise higher rate by 1pp"
+  },
+  {
+   "metric_id": "reforms.higher_rate_plus_1pp",
+   "year": 2028,
+   "value": 4299497163.4904785,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise higher rate by 1pp"
+  },
+  {
+   "metric_id": "reforms.higher_rate_plus_1pp",
+   "year": 2029,
+   "value": 4571859488.912842,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise higher rate by 1pp"
+  },
+  {
+   "metric_id": "reforms.higher_rate_plus_1pp",
+   "year": 2030,
+   "value": 4855874568.11499,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise higher rate by 1pp"
+  },
+  {
+   "metric_id": "reforms.higher_rate_plus_1pp",
+   "year": 2031,
+   "value": 5076062695.494751,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise higher rate by 1pp"
+  },
+  {
+   "metric_id": "reform_targets.higher_rate_plus_1pp",
+   "year": 2025,
+   "value": 3554390535.588806,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise higher rate by 1pp: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.higher_rate_plus_1pp",
+   "year": 2026,
+   "value": 3848232118.605835,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise higher rate by 1pp: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.higher_rate_plus_1pp",
+   "year": 2027,
+   "value": 4084949832.10614,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise higher rate by 1pp: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.higher_rate_plus_1pp",
+   "year": 2028,
+   "value": 4309251584.078491,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise higher rate by 1pp: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.higher_rate_plus_1pp",
+   "year": 2029,
+   "value": 4582787139.6779785,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise higher rate by 1pp: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.higher_rate_plus_1pp",
+   "year": 2030,
+   "value": 4868223711.304871,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise higher rate by 1pp: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.higher_rate_plus_1pp",
+   "year": 2031,
+   "value": 5089139733.0060425,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise higher rate by 1pp: change in income_tax"
+  },
+  {
+   "metric_id": "reforms.personal_allowance_13000",
+   "year": 2025,
+   "value": -3822246893.5978394,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise personal allowance by ~800GBP/year"
+  },
+  {
+   "metric_id": "reforms.personal_allowance_13000",
+   "year": 2026,
+   "value": -3970099818.8598022,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise personal allowance by ~800GBP/year"
+  },
+  {
+   "metric_id": "reforms.personal_allowance_13000",
+   "year": 2027,
+   "value": -4052308604.757263,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise personal allowance by ~800GBP/year"
+  },
+  {
+   "metric_id": "reforms.personal_allowance_13000",
+   "year": 2028,
+   "value": -4101725729.7006836,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise personal allowance by ~800GBP/year"
+  },
+  {
+   "metric_id": "reforms.personal_allowance_13000",
+   "year": 2029,
+   "value": -4183390396.729248,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise personal allowance by ~800GBP/year"
+  },
+  {
+   "metric_id": "reforms.personal_allowance_13000",
+   "year": 2030,
+   "value": -4290067047.536621,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise personal allowance by ~800GBP/year"
+  },
+  {
+   "metric_id": "reforms.personal_allowance_13000",
+   "year": 2031,
+   "value": -2026458045.3624268,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise personal allowance by ~800GBP/year"
+  },
+  {
+   "metric_id": "reform_targets.personal_allowance_13000",
+   "year": 2025,
+   "value": -3954063036.4473877,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise personal allowance by ~800GBP/year: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.personal_allowance_13000",
+   "year": 2026,
+   "value": -4117335693.9852905,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise personal allowance by ~800GBP/year: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.personal_allowance_13000",
+   "year": 2027,
+   "value": -4209427536.251709,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise personal allowance by ~800GBP/year: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.personal_allowance_13000",
+   "year": 2028,
+   "value": -4261098604.1522827,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise personal allowance by ~800GBP/year: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.personal_allowance_13000",
+   "year": 2029,
+   "value": -4343355439.515015,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise personal allowance by ~800GBP/year: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.personal_allowance_13000",
+   "year": 2030,
+   "value": -4452755767.392151,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise personal allowance by ~800GBP/year: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.personal_allowance_13000",
+   "year": 2031,
+   "value": -1780300034.5704956,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise personal allowance by ~800GBP/year: change in income_tax"
+  },
+  {
+   "metric_id": "reforms.child_benefit_additional_25",
+   "year": 2025,
+   "value": -1978301080.5203857,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise child benefit by 25GBP/week per additional child"
+  },
+  {
+   "metric_id": "reforms.child_benefit_additional_25",
+   "year": 2026,
+   "value": -1746066703.213379,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise child benefit by 25GBP/week per additional child"
+  },
+  {
+   "metric_id": "reforms.child_benefit_additional_25",
+   "year": 2027,
+   "value": -1629668073.130188,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise child benefit by 25GBP/week per additional child"
+  },
+  {
+   "metric_id": "reforms.child_benefit_additional_25",
+   "year": 2028,
+   "value": -1530858259.5943604,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise child benefit by 25GBP/week per additional child"
+  },
+  {
+   "metric_id": "reforms.child_benefit_additional_25",
+   "year": 2029,
+   "value": -1428323970.4534912,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise child benefit by 25GBP/week per additional child"
+  },
+  {
+   "metric_id": "reforms.child_benefit_additional_25",
+   "year": 2030,
+   "value": -1325576053.7683105,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise child benefit by 25GBP/week per additional child"
+  },
+  {
+   "metric_id": "reforms.child_benefit_additional_25",
+   "year": 2031,
+   "value": -1219354845.269043,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise child benefit by 25GBP/week per additional child"
+  },
+  {
+   "metric_id": "reform_targets.child_benefit_additional_25",
+   "year": 2025,
+   "value": 2330588637.038988,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise child benefit by 25GBP/week per additional child: change in child_benefit"
+  },
+  {
+   "metric_id": "reform_targets.child_benefit_additional_25",
+   "year": 2026,
+   "value": 2141486725.888443,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise child benefit by 25GBP/week per additional child: change in child_benefit"
+  },
+  {
+   "metric_id": "reform_targets.child_benefit_additional_25",
+   "year": 2027,
+   "value": 2023775956.2239304,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise child benefit by 25GBP/week per additional child: change in child_benefit"
+  },
+  {
+   "metric_id": "reform_targets.child_benefit_additional_25",
+   "year": 2028,
+   "value": 1920161279.352703,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise child benefit by 25GBP/week per additional child: change in child_benefit"
+  },
+  {
+   "metric_id": "reform_targets.child_benefit_additional_25",
+   "year": 2029,
+   "value": 1813779891.1509056,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise child benefit by 25GBP/week per additional child: change in child_benefit"
+  },
+  {
+   "metric_id": "reform_targets.child_benefit_additional_25",
+   "year": 2030,
+   "value": 1704712527.6125221,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise child benefit by 25GBP/week per additional child: change in child_benefit"
+  },
+  {
+   "metric_id": "reform_targets.child_benefit_additional_25",
+   "year": 2031,
+   "value": 1591189257.2969856,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise child benefit by 25GBP/week per additional child: change in child_benefit"
+  },
+  {
+   "metric_id": "reforms.uc_taper_20pc",
+   "year": 2025,
+   "value": -20527842919.80768,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Reduce Universal Credit taper rate to 20%"
+  },
+  {
+   "metric_id": "reforms.uc_taper_20pc",
+   "year": 2026,
+   "value": -21100518260.464355,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Reduce Universal Credit taper rate to 20%"
+  },
+  {
+   "metric_id": "reforms.uc_taper_20pc",
+   "year": 2027,
+   "value": -21383710367.08246,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Reduce Universal Credit taper rate to 20%"
+  },
+  {
+   "metric_id": "reforms.uc_taper_20pc",
+   "year": 2028,
+   "value": -21736411484.46698,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Reduce Universal Credit taper rate to 20%"
+  },
+  {
+   "metric_id": "reforms.uc_taper_20pc",
+   "year": 2029,
+   "value": -21748894837.86438,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Reduce Universal Credit taper rate to 20%"
+  },
+  {
+   "metric_id": "reforms.uc_taper_20pc",
+   "year": 2030,
+   "value": -21876938936.24524,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Reduce Universal Credit taper rate to 20%"
+  },
+  {
+   "metric_id": "reforms.uc_taper_20pc",
+   "year": 2031,
+   "value": -22330952654.634155,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Reduce Universal Credit taper rate to 20%"
+  },
+  {
+   "metric_id": "reform_targets.uc_taper_20pc",
+   "year": 2025,
+   "value": 20519585447.911682,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Reduce Universal Credit taper rate to 20%: change in universal_credit"
+  },
+  {
+   "metric_id": "reform_targets.uc_taper_20pc",
+   "year": 2026,
+   "value": 21090575767.801956,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Reduce Universal Credit taper rate to 20%: change in universal_credit"
+  },
+  {
+   "metric_id": "reform_targets.uc_taper_20pc",
+   "year": 2027,
+   "value": 21373214076.36197,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Reduce Universal Credit taper rate to 20%: change in universal_credit"
+  },
+  {
+   "metric_id": "reform_targets.uc_taper_20pc",
+   "year": 2028,
+   "value": 21725838011.858963,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Reduce Universal Credit taper rate to 20%: change in universal_credit"
+  },
+  {
+   "metric_id": "reform_targets.uc_taper_20pc",
+   "year": 2029,
+   "value": 21742175658.693848,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Reduce Universal Credit taper rate to 20%: change in universal_credit"
+  },
+  {
+   "metric_id": "reform_targets.uc_taper_20pc",
+   "year": 2030,
+   "value": 21868780192.75914,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Reduce Universal Credit taper rate to 20%: change in universal_credit"
+  },
+  {
+   "metric_id": "reform_targets.uc_taper_20pc",
+   "year": 2031,
+   "value": 22322933920.91278,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Reduce Universal Credit taper rate to 20%: change in universal_credit"
+  },
+  {
+   "metric_id": "reforms.employee_nics_main_10pc",
+   "year": 2025,
+   "value": 11064401706.310059,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise Class 1 main employee NICs rate to 10%"
+  },
+  {
+   "metric_id": "reforms.employee_nics_main_10pc",
+   "year": 2026,
+   "value": 11510324242.230225,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise Class 1 main employee NICs rate to 10%"
+  },
+  {
+   "metric_id": "reforms.employee_nics_main_10pc",
+   "year": 2027,
+   "value": 11834108533.458801,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise Class 1 main employee NICs rate to 10%"
+  },
+  {
+   "metric_id": "reforms.employee_nics_main_10pc",
+   "year": 2028,
+   "value": 12138956950.555908,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise Class 1 main employee NICs rate to 10%"
+  },
+  {
+   "metric_id": "reforms.employee_nics_main_10pc",
+   "year": 2029,
+   "value": 12464179087.056396,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise Class 1 main employee NICs rate to 10%"
+  },
+  {
+   "metric_id": "reforms.employee_nics_main_10pc",
+   "year": 2030,
+   "value": 12812306848.333618,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise Class 1 main employee NICs rate to 10%"
+  },
+  {
+   "metric_id": "reforms.employee_nics_main_10pc",
+   "year": 2031,
+   "value": 13289675311.854004,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise Class 1 main employee NICs rate to 10%"
+  },
+  {
+   "metric_id": "reform_targets.employee_nics_main_10pc",
+   "year": 2025,
+   "value": 11329957618.034607,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise Class 1 main employee NICs rate to 10%: change in national_insurance"
+  },
+  {
+   "metric_id": "reform_targets.employee_nics_main_10pc",
+   "year": 2026,
+   "value": 11791691034.516708,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise Class 1 main employee NICs rate to 10%: change in national_insurance"
+  },
+  {
+   "metric_id": "reform_targets.employee_nics_main_10pc",
+   "year": 2027,
+   "value": 12126516761.37503,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise Class 1 main employee NICs rate to 10%: change in national_insurance"
+  },
+  {
+   "metric_id": "reform_targets.employee_nics_main_10pc",
+   "year": 2028,
+   "value": 12442433003.455788,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise Class 1 main employee NICs rate to 10%: change in national_insurance"
+  },
+  {
+   "metric_id": "reform_targets.employee_nics_main_10pc",
+   "year": 2029,
+   "value": 12770326792.025597,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise Class 1 main employee NICs rate to 10%: change in national_insurance"
+  },
+  {
+   "metric_id": "reform_targets.employee_nics_main_10pc",
+   "year": 2030,
+   "value": 13120133297.630943,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise Class 1 main employee NICs rate to 10%: change in national_insurance"
+  },
+  {
+   "metric_id": "reform_targets.employee_nics_main_10pc",
+   "year": 2031,
+   "value": 13607405556.501923,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise Class 1 main employee NICs rate to 10%: change in national_insurance"
+  },
+  {
+   "metric_id": "reforms.vat_standard_plus_2pp",
+   "year": 2025,
+   "value": 30863021596.225952,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise VAT standard rate by 2pp"
+  },
+  {
+   "metric_id": "reforms.vat_standard_plus_2pp",
+   "year": 2026,
+   "value": 31692844228.266296,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise VAT standard rate by 2pp"
+  },
+  {
+   "metric_id": "reforms.vat_standard_plus_2pp",
+   "year": 2027,
+   "value": 32446311527.309143,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise VAT standard rate by 2pp"
+  },
+  {
+   "metric_id": "reforms.vat_standard_plus_2pp",
+   "year": 2028,
+   "value": 33227617036.870483,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise VAT standard rate by 2pp"
+  },
+  {
+   "metric_id": "reforms.vat_standard_plus_2pp",
+   "year": 2029,
+   "value": 34041293064.6344,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise VAT standard rate by 2pp"
+  },
+  {
+   "metric_id": "reforms.vat_standard_plus_2pp",
+   "year": 2030,
+   "value": 34878374451.18152,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise VAT standard rate by 2pp"
+  },
+  {
+   "metric_id": "reforms.vat_standard_plus_2pp",
+   "year": 2031,
+   "value": 35728921387.40991,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise VAT standard rate by 2pp"
+  },
+  {
+   "metric_id": "reform_targets.vat_standard_plus_2pp",
+   "year": 2025,
+   "value": 30863018576.160828,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise VAT standard rate by 2pp: change in vat"
+  },
+  {
+   "metric_id": "reform_targets.vat_standard_plus_2pp",
+   "year": 2026,
+   "value": 31692847105.070984,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise VAT standard rate by 2pp: change in vat"
+  },
+  {
+   "metric_id": "reform_targets.vat_standard_plus_2pp",
+   "year": 2027,
+   "value": 32446312252.777954,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise VAT standard rate by 2pp: change in vat"
+  },
+  {
+   "metric_id": "reform_targets.vat_standard_plus_2pp",
+   "year": 2028,
+   "value": 33227616036.71393,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise VAT standard rate by 2pp: change in vat"
+  },
+  {
+   "metric_id": "reform_targets.vat_standard_plus_2pp",
+   "year": 2029,
+   "value": 34041297530.920654,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise VAT standard rate by 2pp: change in vat"
+  },
+  {
+   "metric_id": "reform_targets.vat_standard_plus_2pp",
+   "year": 2030,
+   "value": 34878372560.67273,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise VAT standard rate by 2pp: change in vat"
+  },
+  {
+   "metric_id": "reform_targets.vat_standard_plus_2pp",
+   "year": 2031,
+   "value": 35728918405.68787,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise VAT standard rate by 2pp: change in vat"
+  },
+  {
+   "metric_id": "reforms.additional_rate_plus_3pp",
+   "year": 2025,
+   "value": 2798505716.117798,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise additional rate by 3pp"
+  },
+  {
+   "metric_id": "reforms.additional_rate_plus_3pp",
+   "year": 2026,
+   "value": 3023336469.8702393,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise additional rate by 3pp"
+  },
+  {
+   "metric_id": "reforms.additional_rate_plus_3pp",
+   "year": 2027,
+   "value": 3213071953.8966064,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise additional rate by 3pp"
+  },
+  {
+   "metric_id": "reforms.additional_rate_plus_3pp",
+   "year": 2028,
+   "value": 3394779601.3947754,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise additional rate by 3pp"
+  },
+  {
+   "metric_id": "reforms.additional_rate_plus_3pp",
+   "year": 2029,
+   "value": 3594508773.6256104,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise additional rate by 3pp"
+  },
+  {
+   "metric_id": "reforms.additional_rate_plus_3pp",
+   "year": 2030,
+   "value": 3822543323.427368,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise additional rate by 3pp"
+  },
+  {
+   "metric_id": "reforms.additional_rate_plus_3pp",
+   "year": 2031,
+   "value": 4125743282.83667,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise additional rate by 3pp"
+  },
+  {
+   "metric_id": "reform_targets.additional_rate_plus_3pp",
+   "year": 2025,
+   "value": 2798504994.492798,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise additional rate by 3pp: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.additional_rate_plus_3pp",
+   "year": 2026,
+   "value": 3023336460.849365,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise additional rate by 3pp: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.additional_rate_plus_3pp",
+   "year": 2027,
+   "value": 3213071862.152466,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise additional rate by 3pp: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.additional_rate_plus_3pp",
+   "year": 2028,
+   "value": 3394780483.9908447,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise additional rate by 3pp: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.additional_rate_plus_3pp",
+   "year": 2029,
+   "value": 3594509097.7042236,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise additional rate by 3pp: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.additional_rate_plus_3pp",
+   "year": 2030,
+   "value": 3822542242.9839478,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise additional rate by 3pp: change in income_tax"
+  },
+  {
+   "metric_id": "reform_targets.additional_rate_plus_3pp",
+   "year": 2031,
+   "value": 4125743186.885071,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise additional rate by 3pp: change in income_tax"
+  },
+  {
+   "metric_id": "reforms.uc_standard_allowance_plus_10pc",
+   "year": 2025,
+   "value": -3038583641.996826,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise the Universal Credit standard allowance by 10%"
+  },
+  {
+   "metric_id": "reforms.uc_standard_allowance_plus_10pc",
+   "year": 2026,
+   "value": -3168138984.6657715,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise the Universal Credit standard allowance by 10%"
+  },
+  {
+   "metric_id": "reforms.uc_standard_allowance_plus_10pc",
+   "year": 2027,
+   "value": -3225527730.8588867,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise the Universal Credit standard allowance by 10%"
+  },
+  {
+   "metric_id": "reforms.uc_standard_allowance_plus_10pc",
+   "year": 2028,
+   "value": -3282105874.806885,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise the Universal Credit standard allowance by 10%"
+  },
+  {
+   "metric_id": "reforms.uc_standard_allowance_plus_10pc",
+   "year": 2029,
+   "value": -3307346412.4144287,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise the Universal Credit standard allowance by 10%"
+  },
+  {
+   "metric_id": "reforms.uc_standard_allowance_plus_10pc",
+   "year": 2030,
+   "value": -3331520951.5113525,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise the Universal Credit standard allowance by 10%"
+  },
+  {
+   "metric_id": "reforms.uc_standard_allowance_plus_10pc",
+   "year": 2031,
+   "value": -3373553161.4073486,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise the Universal Credit standard allowance by 10%"
+  },
+  {
+   "metric_id": "reform_targets.uc_standard_allowance_plus_10pc",
+   "year": 2025,
+   "value": 3041523709.2714844,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise the Universal Credit standard allowance by 10%: change in universal_credit"
+  },
+  {
+   "metric_id": "reform_targets.uc_standard_allowance_plus_10pc",
+   "year": 2026,
+   "value": 3171636381.1340485,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise the Universal Credit standard allowance by 10%: change in universal_credit"
+  },
+  {
+   "metric_id": "reform_targets.uc_standard_allowance_plus_10pc",
+   "year": 2027,
+   "value": 3226364754.786072,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise the Universal Credit standard allowance by 10%: change in universal_credit"
+  },
+  {
+   "metric_id": "reform_targets.uc_standard_allowance_plus_10pc",
+   "year": 2028,
+   "value": 3282674243.09433,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise the Universal Credit standard allowance by 10%: change in universal_credit"
+  },
+  {
+   "metric_id": "reform_targets.uc_standard_allowance_plus_10pc",
+   "year": 2029,
+   "value": 3312220329.68396,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise the Universal Credit standard allowance by 10%: change in universal_credit"
+  },
+  {
+   "metric_id": "reform_targets.uc_standard_allowance_plus_10pc",
+   "year": 2030,
+   "value": 3337058613.859558,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise the Universal Credit standard allowance by 10%: change in universal_credit"
+  },
+  {
+   "metric_id": "reform_targets.uc_standard_allowance_plus_10pc",
+   "year": 2031,
+   "value": 3377518834.7556,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise the Universal Credit standard allowance by 10%: change in universal_credit"
+  },
+  {
+   "metric_id": "reforms.state_pension_plus_10pc",
+   "year": 2025,
+   "value": -8927432756.88562,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise the State Pension by 10%"
+  },
+  {
+   "metric_id": "reforms.state_pension_plus_10pc",
+   "year": 2026,
+   "value": -9142939686.93689,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise the State Pension by 10%"
+  },
+  {
+   "metric_id": "reforms.state_pension_plus_10pc",
+   "year": 2027,
+   "value": -9062725924.750671,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise the State Pension by 10%"
+  },
+  {
+   "metric_id": "reforms.state_pension_plus_10pc",
+   "year": 2028,
+   "value": -9114943836.081665,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise the State Pension by 10%"
+  },
+  {
+   "metric_id": "reforms.state_pension_plus_10pc",
+   "year": 2029,
+   "value": -9306907164.58667,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise the State Pension by 10%"
+  },
+  {
+   "metric_id": "reforms.state_pension_plus_10pc",
+   "year": 2030,
+   "value": -9548783655.451294,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise the State Pension by 10%"
+  },
+  {
+   "metric_id": "reforms.state_pension_plus_10pc",
+   "year": 2031,
+   "value": -9816145928.907349,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform",
+   "partition": null,
+   "label": "Raise the State Pension by 10%"
+  },
+  {
+   "metric_id": "reform_targets.state_pension_plus_10pc",
+   "year": 2025,
+   "value": 12250304093.31047,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise the State Pension by 10%: change in state_pension"
+  },
+  {
+   "metric_id": "reform_targets.state_pension_plus_10pc",
+   "year": 2026,
+   "value": 12711504064.702667,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise the State Pension by 10%: change in state_pension"
+  },
+  {
+   "metric_id": "reform_targets.state_pension_plus_10pc",
+   "year": 2027,
+   "value": 12828062954.26825,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise the State Pension by 10%: change in state_pension"
+  },
+  {
+   "metric_id": "reform_targets.state_pension_plus_10pc",
+   "year": 2028,
+   "value": 13020927023.28334,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise the State Pension by 10%: change in state_pension"
+  },
+  {
+   "metric_id": "reform_targets.state_pension_plus_10pc",
+   "year": 2029,
+   "value": 13405169512.460327,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise the State Pension by 10%: change in state_pension"
+  },
+  {
+   "metric_id": "reform_targets.state_pension_plus_10pc",
+   "year": 2030,
+   "value": 13802157356.131042,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise the State Pension by 10%: change in state_pension"
+  },
+  {
+   "metric_id": "reform_targets.state_pension_plus_10pc",
+   "year": 2031,
+   "value": 14208043135.872528,
+   "suppressed": false,
+   "excludes": [],
+   "exclude_signs": {},
+   "unit": "gbp",
+   "kind": "reform_target",
+   "partition": null,
+   "label": "Raise the State Pension by 10%: change in state_pension"
+  }
+ ],
+ "identities": [
+  {
+   "identity_id": "closure",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "weights:person",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "weights:benunit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "weights:household",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:income_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:income_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:income_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:nics_individual",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:nics_individual",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:nics_individual",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:nics_employer",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:nics_employer",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:nics_employer",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:employer_nics_response_capital",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:employer_nics_response_consumer",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:capital_gains_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:capital_gains_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:capital_gains_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:student_loan_repayments",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:student_loan_repayments",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:student_loan_repayments",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:vat",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:vat",
+   "year": null,
+   "status": "fail",
+   "severity": "fail",
+   "failing_scopes": [
+    "2025",
+    "2026",
+    "2027",
+    "2028",
+    "2029",
+    "2030",
+    "2031"
+   ],
+   "detail": "2025-26: 76 units below zero, a weighted \u00a3-67,632,237 in total; 2026-27: 76 units below zero, a weighted \u00a3-69,450,693 in total; 2027-28: 76 units below zero, a weighted \u00a3-71,101,816 in total; 2028-29: 76 units below zero, a weighted \u00a3-72,813,940 in total; 2029-30: 76 units below zero, a weighted \u00a3-74,597,010 in total; 2030-31: 76 units below zero, a weighted \u00a3-76,431,349 in total; 2031-32: 76 units below zero, a weighted \u00a3-78,295,212 in total"
+  },
+  {
+   "identity_id": "inert:vat",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:vat_change",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:private_school_vat",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:private_school_vat",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:fuel_duty",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:fuel_duty",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:fuel_duty",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:alcohol_duty",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:alcohol_duty",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:alcohol_duty",
+   "year": null,
+   "status": "warn",
+   "severity": "warn",
+   "failing_scopes": [
+    "2025",
+    "2026",
+    "2027",
+    "2028",
+    "2029",
+    "2030",
+    "2031"
+   ],
+   "detail": "2025-26: zero for every unit; 2026-27: zero for every unit; 2027-28: zero for every unit; 2028-29: zero for every unit; 2029-30: zero for every unit; 2030-31: zero for every unit; 2031-32: zero for every unit"
+  },
+  {
+   "identity_id": "finite:tobacco_duty",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:tobacco_duty",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:tobacco_duty",
+   "year": null,
+   "status": "warn",
+   "severity": "warn",
+   "failing_scopes": [
+    "2025",
+    "2026",
+    "2027",
+    "2028",
+    "2029",
+    "2030",
+    "2031"
+   ],
+   "detail": "2025-26: zero for every unit; 2026-27: zero for every unit; 2027-28: zero for every unit; 2028-29: zero for every unit; 2029-30: zero for every unit; 2030-31: zero for every unit; 2031-32: zero for every unit"
+  },
+  {
+   "identity_id": "finite:vehicle_excise_duty",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:vehicle_excise_duty",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:vehicle_excise_duty",
+   "year": null,
+   "status": "warn",
+   "severity": "warn",
+   "failing_scopes": [
+    "2025",
+    "2026",
+    "2027",
+    "2028",
+    "2029",
+    "2030",
+    "2031"
+   ],
+   "detail": "2025-26: zero for every unit; 2026-27: zero for every unit; 2027-28: zero for every unit; 2028-29: zero for every unit; 2029-30: zero for every unit; 2030-31: zero for every unit; 2031-32: zero for every unit"
+  },
+  {
+   "identity_id": "finite:tv_licence",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:tv_licence",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:tv_licence",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:council_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:council_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:council_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:high_value_council_tax_surcharge",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:high_value_council_tax_surcharge",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:high_value_council_tax_surcharge",
+   "year": null,
+   "status": "warn",
+   "severity": "warn",
+   "failing_scopes": [
+    "2025",
+    "2026",
+    "2027"
+   ],
+   "detail": "2025-26: zero for every unit; 2026-27: zero for every unit; 2027-28: zero for every unit"
+  },
+  {
+   "identity_id": "finite:domestic_rates",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:domestic_rates",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:domestic_rates",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:business_rates",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:business_rates",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:business_rates",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:sdlt_household",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:sdlt_household",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:sdlt_household",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:land_transaction_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:land_transaction_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:land_transaction_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:land_and_buildings_transaction_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:land_and_buildings_transaction_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:land_and_buildings_transaction_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:corporate_incident_tax_change",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:consumer_incident_tax_change",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:wealth_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:wealth_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:non_primary_residence_wealth_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:non_primary_residence_wealth_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:land_value_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:land_value_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:carbon_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:carbon_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:state_pension",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:state_pension",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:state_pension",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:pension_credit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:pension_credit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:pension_credit",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:winter_fuel_payment",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:winter_fuel_payment",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:winter_fuel_payment",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:pawhp",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:pawhp",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:pawhp",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:universal_credit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:universal_credit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:universal_credit",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:housing_benefit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:housing_benefit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:housing_benefit",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:council_tax_reduction",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:council_tax_reduction",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:council_tax_reduction",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:esa_income",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:esa_income",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:esa_income",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:esa_contrib",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:esa_contrib",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:esa_contrib",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:jsa_income",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:jsa_income",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:jsa_income",
+   "year": null,
+   "status": "warn",
+   "severity": "warn",
+   "failing_scopes": [
+    "2026",
+    "2027",
+    "2028",
+    "2029",
+    "2030",
+    "2031"
+   ],
+   "detail": "2026-27: zero for every unit; 2027-28: zero for every unit; 2028-29: zero for every unit; 2029-30: zero for every unit; 2030-31: zero for every unit; 2031-32: zero for every unit"
+  },
+  {
+   "identity_id": "finite:jsa_contrib",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:jsa_contrib",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:jsa_contrib",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:income_support",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:income_support",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:income_support",
+   "year": null,
+   "status": "warn",
+   "severity": "warn",
+   "failing_scopes": [
+    "2026",
+    "2027",
+    "2028",
+    "2029",
+    "2030",
+    "2031"
+   ],
+   "detail": "2026-27: zero for every unit; 2027-28: zero for every unit; 2028-29: zero for every unit; 2029-30: zero for every unit; 2030-31: zero for every unit; 2031-32: zero for every unit"
+  },
+  {
+   "identity_id": "finite:working_tax_credit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:working_tax_credit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:working_tax_credit",
+   "year": null,
+   "status": "warn",
+   "severity": "warn",
+   "failing_scopes": [
+    "2025",
+    "2026",
+    "2027",
+    "2028",
+    "2029",
+    "2030",
+    "2031"
+   ],
+   "detail": "2025-26: zero for every unit; 2026-27: zero for every unit; 2027-28: zero for every unit; 2028-29: zero for every unit; 2029-30: zero for every unit; 2030-31: zero for every unit; 2031-32: zero for every unit"
+  },
+  {
+   "identity_id": "finite:child_tax_credit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:child_tax_credit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:child_tax_credit",
+   "year": null,
+   "status": "warn",
+   "severity": "warn",
+   "failing_scopes": [
+    "2025",
+    "2026",
+    "2027",
+    "2028",
+    "2029",
+    "2030",
+    "2031"
+   ],
+   "detail": "2025-26: zero for every unit; 2026-27: zero for every unit; 2027-28: zero for every unit; 2028-29: zero for every unit; 2029-30: zero for every unit; 2030-31: zero for every unit; 2031-32: zero for every unit"
+  },
+  {
+   "identity_id": "finite:incapacity_benefit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:incapacity_benefit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:incapacity_benefit",
+   "year": null,
+   "status": "warn",
+   "severity": "warn",
+   "failing_scopes": [
+    "2025",
+    "2026",
+    "2027",
+    "2028",
+    "2029",
+    "2030",
+    "2031"
+   ],
+   "detail": "2025-26: zero for every unit; 2026-27: zero for every unit; 2027-28: zero for every unit; 2028-29: zero for every unit; 2029-30: zero for every unit; 2030-31: zero for every unit; 2031-32: zero for every unit"
+  },
+  {
+   "identity_id": "finite:pip",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:pip",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:pip",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:dla",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:dla",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:dla",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:attendance_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:attendance_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:attendance_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:carers_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:carers_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:carers_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:carer_support_payment",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:carer_support_payment",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:carer_support_payment",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:scottish_carer_supplement",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:scottish_carer_supplement",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:scottish_carer_supplement",
+   "year": null,
+   "status": "warn",
+   "severity": "warn",
+   "failing_scopes": [
+    "2025"
+   ],
+   "detail": "2025-26: zero for every unit"
+  },
+  {
+   "identity_id": "finite:sda",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:sda",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:sda",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:iidb",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:iidb",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:iidb",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:afcs",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:afcs",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:afcs",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:child_benefit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:child_benefit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:child_benefit",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:scottish_child_payment",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:scottish_child_payment",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:scottish_child_payment",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:two_child_limit_payment",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:two_child_limit_payment",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:healthy_start_vouchers",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:healthy_start_vouchers",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:healthy_start_vouchers",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:maternity_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:maternity_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:maternity_allowance",
+   "year": null,
+   "status": "warn",
+   "severity": "warn",
+   "failing_scopes": [
+    "2025",
+    "2026",
+    "2027",
+    "2028",
+    "2029",
+    "2030",
+    "2031"
+   ],
+   "detail": "2025-26: zero for every unit; 2026-27: zero for every unit; 2027-28: zero for every unit; 2028-29: zero for every unit; 2029-30: zero for every unit; 2030-31: zero for every unit; 2031-32: zero for every unit"
+  },
+  {
+   "identity_id": "finite:statutory_maternity_pay",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:statutory_maternity_pay",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:statutory_maternity_pay",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:statutory_paternity_pay",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:statutory_paternity_pay",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:statutory_paternity_pay",
+   "year": null,
+   "status": "warn",
+   "severity": "warn",
+   "failing_scopes": [
+    "2025",
+    "2026",
+    "2027",
+    "2028",
+    "2029",
+    "2030",
+    "2031"
+   ],
+   "detail": "2025-26: zero for every unit; 2026-27: zero for every unit; 2027-28: zero for every unit; 2028-29: zero for every unit; 2029-30: zero for every unit; 2030-31: zero for every unit; 2031-32: zero for every unit"
+  },
+  {
+   "identity_id": "finite:statutory_sick_pay",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:statutory_sick_pay",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:statutory_sick_pay",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:ssmg",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:ssmg",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:ssmg",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:bsp",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:bsp",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:bsp",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:tax_free_childcare",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:tax_free_childcare",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:tax_free_childcare",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:extended_childcare_entitlement",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:extended_childcare_entitlement",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:extended_childcare_entitlement",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:universal_childcare_entitlement",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:universal_childcare_entitlement",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:universal_childcare_entitlement",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:targeted_childcare_entitlement",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:targeted_childcare_entitlement",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:targeted_childcare_entitlement",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:care_to_learn",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:care_to_learn",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:care_to_learn",
+   "year": null,
+   "status": "warn",
+   "severity": "warn",
+   "failing_scopes": [
+    "2025",
+    "2026",
+    "2027",
+    "2028",
+    "2029",
+    "2030",
+    "2031"
+   ],
+   "detail": "2025-26: zero for every unit; 2026-27: zero for every unit; 2027-28: zero for every unit; 2028-29: zero for every unit; 2029-30: zero for every unit; 2030-31: zero for every unit; 2031-32: zero for every unit"
+  },
+  {
+   "identity_id": "finite:childcare_grant",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:childcare_grant",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:childcare_grant",
+   "year": null,
+   "status": "warn",
+   "severity": "warn",
+   "failing_scopes": [
+    "2025",
+    "2026",
+    "2027",
+    "2028",
+    "2029",
+    "2030",
+    "2031"
+   ],
+   "detail": "2025-26: zero for every unit; 2026-27: zero for every unit; 2027-28: zero for every unit; 2028-29: zero for every unit; 2029-30: zero for every unit; 2030-31: zero for every unit; 2031-32: zero for every unit"
+  },
+  {
+   "identity_id": "finite:parents_learning_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:parents_learning_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:parents_learning_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:adult_dependants_grant",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:adult_dependants_grant",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:adult_dependants_grant",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:travel_grant",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:travel_grant",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:travel_grant",
+   "year": null,
+   "status": "warn",
+   "severity": "warn",
+   "failing_scopes": [
+    "2025",
+    "2026",
+    "2027",
+    "2028",
+    "2029",
+    "2030",
+    "2031"
+   ],
+   "detail": "2025-26: zero for every unit; 2026-27: zero for every unit; 2027-28: zero for every unit; 2028-29: zero for every unit; 2029-30: zero for every unit; 2030-31: zero for every unit; 2031-32: zero for every unit"
+  },
+  {
+   "identity_id": "finite:disabled_students_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:disabled_students_allowance",
+   "year": null,
+   "status": "skipped",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": "2025-26: not checked, from fewer than 10 records; 2026-27: not checked, from fewer than 10 records; 2027-28: not checked, from fewer than 10 records; 2028-29: not checked, from fewer than 10 records; 2029-30: not checked, from fewer than 10 records; 2030-31: not checked, from fewer than 10 records; 2031-32: not checked, from fewer than 10 records"
+  },
+  {
+   "identity_id": "inert:disabled_students_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:bursary_fund_16_to_19",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:bursary_fund_16_to_19",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:bursary_fund_16_to_19",
+   "year": null,
+   "status": "warn",
+   "severity": "warn",
+   "failing_scopes": [
+    "2025",
+    "2026",
+    "2027",
+    "2028",
+    "2029",
+    "2030",
+    "2031"
+   ],
+   "detail": "2025-26: zero for every unit; 2026-27: zero for every unit; 2027-28: zero for every unit; 2028-29: zero for every unit; 2029-30: zero for every unit; 2030-31: zero for every unit; 2031-32: zero for every unit"
+  },
+  {
+   "identity_id": "finite:nhs_spending",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:nhs_spending",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:nhs_spending",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:dfe_education_spending",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:dfe_education_spending",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:dfe_education_spending",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:dft_subsidy_spending",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:dft_subsidy_spending",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "inert:dft_subsidy_spending",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:cost_of_living_support_payment",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:cost_of_living_support_payment",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:energy_bills_rebate",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:energy_bills_rebate",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:epg_subsidy",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "sign:epg_subsidy",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:basic_income",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:disability_basic_income",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:other_public_spending_budget_change",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "overlap_guard:property_transaction_tax_geography",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "overlap_guard:local_tax_geography",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "identity:gov_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "identity:household_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "identity:pre_budget_change_household_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "identity:gov_spending",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "identity:household_benefits",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "identity:gov_balance",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "identity:household_net_income",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "available:absolute_bhc",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "bounds:poverty_rates",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "available:children",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "available:working_age_adults",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "available:pensioners",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "available:absolute_ahc",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "available:relative_bhc",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "available:relative_ahc",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:income_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:national_insurance",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:ni_employer",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:employer_ni_response_capital_incidence",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:employer_ni_response_consumer_incidence",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:capital_gains_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:student_loan_repayments",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:vat",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:vat_change",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:private_school_vat",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:fuel_duty",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:alcohol_duty",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:tobacco_duty",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:vehicle_excise_duty",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:tv_licence",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:council_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:high_value_council_tax_surcharge",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:domestic_rates",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:business_rates",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:expected_sdlt",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:expected_ltt",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:expected_lbtt",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:corporate_incident_tax_revenue_change",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:consumer_incident_tax_revenue_change",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:wealth_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:non_primary_residence_wealth_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:LVT",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:carbon_tax",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:state_pension",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:pension_credit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:winter_fuel_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:pawhp",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:universal_credit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:housing_benefit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:council_tax_benefit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:esa_income",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:esa_contrib",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:jsa_income",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:jsa_contrib",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:income_support",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:working_tax_credit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:child_tax_credit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:incapacity_benefit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:pip",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:dla",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:attendance_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:carers_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:carer_support_payment",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:scottish_carer_supplement",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:sda",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:iidb",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:afcs",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:child_benefit",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:scottish_child_payment",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:two_child_limit_payment",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:healthy_start_vouchers",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:maternity_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:statutory_maternity_pay",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:statutory_paternity_pay",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:statutory_sick_pay",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:ssmg",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:bsp",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:tax_free_childcare",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:extended_childcare_entitlement",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:universal_childcare_entitlement",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:targeted_childcare_entitlement",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:care_to_learn",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:childcare_grant",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:parents_learning_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:adult_dependants_grant",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:travel_grant",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:disabled_students_allowance",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:bursary_fund_16_to_19",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:nhs_spending",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:dfe_education_spending",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:dft_subsidy_spending",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:cost_of_living_support_payment",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:energy_bills_rebate",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:epg_subsidy",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:basic_income",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:disability_basic_income",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:other_public_spending_budget_change",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:corporate_sdlt",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "double_count_present:corporate_sdlt",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:reset_reproduces_baseline",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "counted_once:ran",
+   "year": null,
+   "status": "pass",
+   "severity": "warn",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "determinism:rebuilt_baseline",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "responsive:basic_rate_plus_1pp",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "responsive:higher_rate_plus_1pp",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "responsive:personal_allowance_13000",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "responsive:child_benefit_additional_25",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "responsive:uc_taper_20pc",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "responsive:employee_nics_main_10pc",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "responsive:vat_standard_plus_2pp",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "responsive:additional_rate_plus_3pp",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "responsive:uc_standard_allowance_plus_10pc",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "responsive:state_pension_plus_10pc",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "reform_runs:basic_rate_plus_1pp",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "reform_runs:higher_rate_plus_1pp",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "reform_runs:personal_allowance_13000",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "reform_runs:child_benefit_additional_25",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "reform_runs:uc_taper_20pc",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "reform_runs:employee_nics_main_10pc",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "reform_runs:vat_standard_plus_2pp",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "reform_runs:additional_rate_plus_3pp",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "reform_runs:uc_standard_allowance_plus_10pc",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "reform_runs:state_pension_plus_10pc",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  },
+  {
+   "identity_id": "finite:metrics",
+   "year": null,
+   "status": "pass",
+   "severity": "fail",
+   "failing_scopes": [],
+   "detail": ""
+  }
+ ]
+}
\ No newline at end of file
diff --git a/tests/fixtures/uk_metrics/standard_reforms.yaml b/tests/fixtures/uk_metrics/standard_reforms.yaml
new file mode 100644
index 00000000..402d8ae1
--- /dev/null
+++ b/tests/fixtures/uk_metrics/standard_reforms.yaml
@@ -0,0 +1,150 @@
+# The standard reforms the aggregate-impact report costs on each side.
+#
+# Each reform is either absolute (`parameters`: values applied from 2023, as
+# the reform-cost fixtures in tests/microsimulation/reforms_config.yaml do) or
+# relative (`scale`: each listed parameter is multiplied by the factor in
+# every year of the horizon, starting from that year's baseline value).
+#
+# `responds` states what the reform must do: the model variable whose
+# weighted total it must move, the direction, and the least it must move it
+# in every year of the horizon. A reform that moves its target by less, by
+# nothing or the wrong way fails the `responsive:` check, so a reform the
+# model silently ignores cannot pass as a small number.
+#
+# Calibration (adopted under cos d1344, 15 October 2026). Each minimum is a
+# fifth of the reform's smallest yearly effect over 2025-26 to 2031-32 on the
+# certified pair, rounded down to two significant figures; parse_reforms
+# refuses a minimum that does not follow from its smallest_effect. Recalibrate
+# with `python -m policyengine_uk.utils.aggregate_impact calibrate` on fresh
+# measurements, and change both numbers together.
+# - Pair: uk-data 1.58.1 (enhanced_frs_2024_25, sha256 056e80fd), measured
+#   with policyengine-uk 2.130.1 on policyengine-core 3.33.0 (the certified
+#   model, policyengine.py bundle uk-6.4.3) and with 2.132.6 on core 3.33.2.
+#   Every target change was identical between the two.
+# - What the check must catch: a PolicyEngine UK backtest (11 October 2026,
+#   cited in the Budget CI plan) found the 10% UC standard allowance rise
+#   returning exactly £0 from 2.45.1 to 2.75.3, and the 10% State Pension
+#   rise barely moving from 2.88.10 to 2.102.6.
+# - What it must not catch: legitimate moves. The fixture receipts show the
+#   2029-30 costings moving by up to 18% between data and model versions,
+#   and 38% for the VAT rise (21.2 on uk-data 1.40.3 against 34.5 on 1.58.0,
+#   from recorded consumption). A fifth fails a reform only once it has lost
+#   80% of its smallest yearly effect, twice the largest move seen.
+# - Alternatives considered: a tenth (fails only after a 90% loss, too late
+#   for "barely moving"); a half (the VAT data change kept 62% of the
+#   effect, so two such changes in a row would fail a correct model);
+#   per-year minimums (tighter in years where an absolute reform is worth
+#   more, but every horizon or data change would need a full table); an
+#   external anchor such as HMRC's ready reckoner (that tests calibration,
+#   not whether the reform is wired through); and a sign check alone
+#   (passes "barely moving").
+# - A single minimum is a wiring check, not a per-year 20% guarantee: the
+#   personal allowance minimum (£0.35bn) is 20% of its 2031-32 effect
+#   (£1.78bn) but 8% of its 2030-31 effect (£4.45bn), so a 90% loss in
+#   2030-31 alone would still pass.
+#
+# An absolute reform shrinks as the baseline is uprated towards it: the
+# 13,000 personal allowance is worth £4.45bn in 2030-31 but £1.78bn in
+# 2031-32, when the baseline allowance rises to 12,830. From 2032-33 the
+# baseline (13,090) exceeds it, so before the horizon reaches that year,
+# make that reform relative.
+schema_version: 1
+
+calibration:
+  fraction: 0.2
+
+reforms:
+  - id: basic_rate_plus_1pp
+    name: Raise basic rate by 1pp
+    parameters:
+      gov.hmrc.income_tax.rates.uk[0].rate: 0.21
+    responds:
+      variable: income_tax
+      direction: up
+      minimum: 1200000000
+      smallest_effect: 6366582049  # 2025-26
+  - id: higher_rate_plus_1pp
+    name: Raise higher rate by 1pp
+    parameters:
+      gov.hmrc.income_tax.rates.uk[1].rate: 0.42
+    responds:
+      variable: income_tax
+      direction: up
+      minimum: 710000000
+      smallest_effect: 3554390536  # 2025-26
+  - id: personal_allowance_13000
+    name: Raise personal allowance by ~800GBP/year
+    parameters:
+      gov.hmrc.income_tax.allowances.personal_allowance.amount: 13000
+    responds:
+      variable: income_tax
+      direction: down
+      minimum: 350000000
+      smallest_effect: 1780300035  # 2031-32
+  - id: child_benefit_additional_25
+    name: Raise child benefit by 25GBP/week per additional child
+    parameters:
+      gov.hmrc.child_benefit.amount.additional: 25
+    responds:
+      variable: child_benefit
+      direction: up
+      minimum: 310000000
+      smallest_effect: 1591189257  # 2031-32
+  - id: uc_taper_20pc
+    name: Reduce Universal Credit taper rate to 20%
+    parameters:
+      gov.dwp.universal_credit.means_test.reduction_rate: 0.2
+    responds:
+      variable: universal_credit
+      direction: up
+      minimum: 4100000000
+      smallest_effect: 20519585448  # 2025-26
+  - id: employee_nics_main_10pc
+    name: Raise Class 1 main employee NICs rate to 10%
+    parameters:
+      gov.hmrc.national_insurance.class_1.rates.employee.main: 0.1
+    responds:
+      variable: national_insurance
+      direction: up
+      minimum: 2200000000
+      smallest_effect: 11329957618  # 2025-26
+  - id: vat_standard_plus_2pp
+    name: Raise VAT standard rate by 2pp
+    parameters:
+      gov.hmrc.vat.standard_rate: 0.22
+    responds:
+      variable: vat
+      direction: up
+      minimum: 6100000000
+      smallest_effect: 30863018576  # 2025-26
+  - id: additional_rate_plus_3pp
+    name: Raise additional rate by 3pp
+    parameters:
+      gov.hmrc.income_tax.rates.uk[2].rate: 0.48
+    responds:
+      variable: income_tax
+      direction: up
+      minimum: 550000000
+      smallest_effect: 2798504994  # 2025-26
+  - id: uc_standard_allowance_plus_10pc
+    name: Raise the Universal Credit standard allowance by 10%
+    scale:
+      gov.dwp.universal_credit.standard_allowance.amount.SINGLE_YOUNG: 1.1
+      gov.dwp.universal_credit.standard_allowance.amount.SINGLE_OLD: 1.1
+      gov.dwp.universal_credit.standard_allowance.amount.COUPLE_YOUNG: 1.1
+      gov.dwp.universal_credit.standard_allowance.amount.COUPLE_OLD: 1.1
+    responds:
+      variable: universal_credit
+      direction: up
+      minimum: 600000000
+      smallest_effect: 3041523709  # 2025-26
+  - id: state_pension_plus_10pc
+    name: Raise the State Pension by 10%
+    scale:
+      gov.dwp.state_pension.new_state_pension.amount: 1.1
+      gov.dwp.state_pension.basic_state_pension.amount: 1.1
+    responds:
+      variable: state_pension
+      direction: up
+      minimum: 2400000000
+      smallest_effect: 12250304093  # 2025-26
diff --git a/tests/fixtures/uk_replay/DIGESTS.json b/tests/fixtures/uk_replay/DIGESTS.json
index b7e61f62..c7f000d9 100644
--- a/tests/fixtures/uk_replay/DIGESTS.json
+++ b/tests/fixtures/uk_replay/DIGESTS.json
@@ -3,6 +3,6 @@
   "request_id": "2e4dd8350bfdc5a39780e002c14ab49c02cbf28b77b222af420e38c68bb945cf"
  },
  "replay_receipt_golden.json": {
-  "receipt_sha256": "a070ac40cb4e6d7daef520cfc1be4ed4958cdd4fe275975dc6d55e856d4ff515"
+  "receipt_sha256": "5cbc44c1b79506a6410c12b437a7451a0e1387ab6a133b9cb8dc0bf71b396650"
  }
 }
diff --git a/tests/fixtures/uk_replay/README.md b/tests/fixtures/uk_replay/README.md
index 98c80f60..398abb5c 100644
--- a/tests/fixtures/uk_replay/README.md
+++ b/tests/fixtures/uk_replay/README.md
@@ -1,7 +1,7 @@
 # UK replay interface golden fixtures
 
 Vendored unchanged from PolicyEngine/policyengine-scorecard at commit
-`7cfa3f9` (`tests/fixtures/uk_replay/`), the W6 side of the pre-promotion
+`dc040de` (`tests/fixtures/uk_replay/`), the W6 side of the pre-promotion
 replay interface. The scorecard computes digests with `rfc8785==0.1.4`;
 `tests/test_canonical_json.py` checks that
 `policyengine.provenance.canonical` produces the same bytes and digests.
diff --git a/tests/fixtures/uk_replay/replay_receipt_golden.jcs b/tests/fixtures/uk_replay/replay_receipt_golden.jcs
index 871f1d32..d542e5bd 100644
--- a/tests/fixtures/uk_replay/replay_receipt_golden.jcs
+++ b/tests/fixtures/uk_replay/replay_receipt_golden.jcs
@@ -1 +1 @@
-{"corpus":{"corpus_version":"uk-fiscal-event-replay/1","events":{"spring_budget_2023":{"artifacts":8,"comparison_provenance_sha256":"8888888888888888888888888888888888888888888888888888888888888888","full_supported_grid_complete":true,"inert_measure_years":[],"registry_sha256":"5555555555555555555555555555555555555555555555555555555555555555","run_manifest_sha256":"6666666666666666666666666666666666666666666666666666666666666666","staging_manifest_sha256":"7777777777777777777777777777777777777777777777777777777777777777","supported_years":[2024,2025,2026,2027],"unsupported_years":[{"reason":"before_data_year","year":2023}]}}},"diff":{"base_variant_id":"pe5.0.2_uk2.89.2_populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z","diff_sha256":"9999999999999999999999999999999999999999999999999999999999999999","flag_counts":{"construction":0,"coverage":0,"inertness":0,"mapping":0,"numerical":2,"structural":1,"support":1},"flagged_rows":[{"base_gbp":-15000000000000000,"event":"spring_budget_2023","flags":["numerical"],"head_gbp":0.30000000000000004,"key":"universal_credit","level":"head","measure_key":"spring_budget_2023__uc_childcare_cap_increase","year":2024},{"base_gbp":1e-7,"event":"spring_budget_2023","flags":["numerical","structural"],"head_gbp":-123456789.125,"key":"total","level":"measure_year","measure_key":"spring_budget_2023__uc_childcare_cap_increase","year":2025},{"base_gbp":-2500000,"event":"spring_budget_2023","flags":["support"],"head_gbp":null,"key":"obr-pmd-row-123","level":"source_row","measure_key":"spring_budget_2023__uc_childcare_cap_increase","year":2023}]},"execution":{"dataset_sha256":"5ba399ea5ab9a6c7d4e9aaf6c623080beb34d9ad5f330f6244dbad72b8d960b6","packaged_bundle_manifest_sha256":"8633ad814c9c85e53c7c513f69c4c7b1500a03cd2f6ef133d6e2ea8346b2db54","packages":{"policyengine":{"record_sha256":"1111111111111111111111111111111111111111111111111111111111111111","version":"6.2.6"},"policyengine-core":{"record_sha256":"3333333333333333333333333333333333333333333333333333333333333333","version":"3.33.0"},"policyengine-uk":{"record_sha256":"2222222222222222222222222222222222222222222222222222222222222222","version":"2.125.1"}},"platform":"Golden fixture (synthetic values; café £)","python":"3.12.14","release_bundle":{"bundle_id":"uk-6.2.6","certified_by":"policyengine.py bundle certification","certified_data_artifact_sha256":"5ba399ea5ab9a6c7d4e9aaf6c623080beb34d9ad5f330f6244dbad72b8d960b6","certified_data_build_id":"policyengine-uk-data-1.58.0","compatibility_basis":"legacy_compatible_model_package","country_id":"uk","data_build_fingerprint":"sha256:c094d7006fdd81df220090284212f16bbffb15624643bc442c4bb3e1b1e5dac8","data_build_model_git_sha":"4cbedbecca352f52dffe47e849553c2759a00711","data_build_model_version":"2.122.2","data_package":"policyengine-uk-data","data_version":"1.58.0","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":"policyengine-uk","model_version":"2.125.1","policyengine_version":"6.2.6"},"requirements_sha256":"4444444444444444444444444444444444444444444444444444444444444444","runner":"github-actions ubuntu-latest"},"integrity":{"failures":[],"status":"complete"},"rejection":null,"request_id":"2e4dd8350bfdc5a39780e002c14ab49c02cbf28b77b222af420e38c68bb945cf","results":{"commit":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","path":"requests/2e4dd8350bfdc5a39780e002c14ab49c02cbf28b77b222af420e38c68bb945cf/results/","ref":"replay-state","repository":"PolicyEngine/policyengine-scorecard"},"schema":"policyengine.uk-replay-receipt/v1","status":"complete","variant_id":"pe6.2.6_uk2.125.1_policyengine-uk-data-1.58.0","variant_identity_sha256":"0000000000000000000000000000000000000000000000000000000000000000"}
\ No newline at end of file
+{"corpus":{"corpus_version":"uk-fiscal-event-replay/1","events":{"spring_budget_2023":{"artifacts":8,"comparison_provenance_sha256":"8888888888888888888888888888888888888888888888888888888888888888","full_supported_grid_complete":true,"inert_measure_years":[],"registry_sha256":"5555555555555555555555555555555555555555555555555555555555555555","run_manifest_sha256":"6666666666666666666666666666666666666666666666666666666666666666","staging_manifest_sha256":"7777777777777777777777777777777777777777777777777777777777777777","supported_years":[2024,2025,2026,2027],"unsupported_years":[{"reason":"before_data_year","year":2023}]}}},"diff":{"base_variant_id":"pe5.0.2_uk2.89.2_populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z","diff_sha256":"9999999999999999999999999999999999999999999999999999999999999999","flag_counts":{"construction":0,"coverage":0,"inertness":0,"mapping":0,"numerical":2,"structural":1,"support":1},"flagged_rows":[{"base_gbp":-15000000000000000,"event":"spring_budget_2023","flags":["numerical"],"head_gbp":0.30000000000000004,"key":"universal_credit","level":"head","measure_key":"spring_budget_2023__uc_childcare_cap_increase","year":2024},{"base_gbp":1e-7,"event":"spring_budget_2023","flags":["numerical","structural"],"head_gbp":-123456789.125,"key":"total","level":"measure_year","measure_key":"spring_budget_2023__uc_childcare_cap_increase","year":2025},{"base_gbp":-2500000,"event":"spring_budget_2023","flags":["support"],"head_gbp":null,"key":"obr-pmd-row-123","level":"source_row","measure_key":"spring_budget_2023__uc_childcare_cap_increase","year":2023}]},"execution":{"dataset_sha256":"5ba399ea5ab9a6c7d4e9aaf6c623080beb34d9ad5f330f6244dbad72b8d960b6","packaged_bundle_manifest_sha256":"8633ad814c9c85e53c7c513f69c4c7b1500a03cd2f6ef133d6e2ea8346b2db54","packages":{"policyengine":{"record_sha256":"1111111111111111111111111111111111111111111111111111111111111111","version":"6.2.6"},"policyengine-core":{"record_sha256":"3333333333333333333333333333333333333333333333333333333333333333","version":"3.33.0"},"policyengine-uk":{"record_sha256":"2222222222222222222222222222222222222222222222222222222222222222","version":"2.125.1"}},"platform":"Golden fixture (synthetic values; café £)","python":"3.12.14","release_bundle":{"bundle_id":"uk-6.2.6","certified_by":"policyengine.py bundle certification","certified_data_artifact_sha256":"5ba399ea5ab9a6c7d4e9aaf6c623080beb34d9ad5f330f6244dbad72b8d960b6","certified_data_build_id":"policyengine-uk-data-1.58.0","compatibility_basis":"legacy_compatible_model_package","country_id":"uk","data_build_fingerprint":"sha256:c094d7006fdd81df220090284212f16bbffb15624643bc442c4bb3e1b1e5dac8","data_build_model_git_sha":"4cbedbecca352f52dffe47e849553c2759a00711","data_build_model_version":"2.122.2","data_package":"policyengine-uk-data","data_version":"1.58.0","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":"policyengine-uk","model_version":"2.125.1","policyengine_version":"6.2.6"},"requirements_sha256":"4444444444444444444444444444444444444444444444444444444444444444","runner":"github-actions ubuntu-latest"},"integrity":{"failures":[],"status":"complete"},"rejection":null,"request_id":"2e4dd8350bfdc5a39780e002c14ab49c02cbf28b77b222af420e38c68bb945cf","results":{"commit":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","manifest_sha256":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","path":"requests/2e4dd8350bfdc5a39780e002c14ab49c02cbf28b77b222af420e38c68bb945cf/results/","ref":"replay-state","repository":"PolicyEngine/policyengine-scorecard"},"schema":"policyengine.uk-replay-receipt/v1","status":"complete","variant_id":"pe6.2.6_uk2.125.1_policyengine-uk-data-1.58.0","variant_identity_sha256":"0000000000000000000000000000000000000000000000000000000000000000"}
\ No newline at end of file
diff --git a/tests/fixtures/uk_replay/replay_receipt_golden.json b/tests/fixtures/uk_replay/replay_receipt_golden.json
index 433f095c..87898936 100644
--- a/tests/fixtures/uk_replay/replay_receipt_golden.json
+++ b/tests/fixtures/uk_replay/replay_receipt_golden.json
@@ -131,7 +131,8 @@
   "repository": "PolicyEngine/policyengine-scorecard",
   "ref": "replay-state",
   "commit": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
-  "path": "requests/2e4dd8350bfdc5a39780e002c14ab49c02cbf28b77b222af420e38c68bb945cf/results/"
+  "path": "requests/2e4dd8350bfdc5a39780e002c14ab49c02cbf28b77b222af420e38c68bb945cf/results/",
+  "manifest_sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
  },
- "receipt_sha256": "a070ac40cb4e6d7daef520cfc1be4ed4958cdd4fe275975dc6d55e856d4ff515"
+ "receipt_sha256": "5cbc44c1b79506a6410c12b437a7451a0e1387ab6a133b9cb8dc0bf71b396650"
 }
diff --git a/tests/qualification_fixtures.py b/tests/qualification_fixtures.py
new file mode 100644
index 00000000..5f27153b
--- /dev/null
+++ b/tests/qualification_fixtures.py
@@ -0,0 +1,102 @@
+"""Shared builders for qualification tests (no network, no credentials)."""
+
+from __future__ import annotations
+
+import json
+from pathlib import Path
+
+from policyengine.provenance.qualification import (
+    REQUIRED_GATES,
+    CandidatePair,
+    GateResult,
+    IssuedBy,
+    PinnedData,
+    PinnedWheel,
+    QualificationHarness,
+    QualificationReceipt,
+    qualification_verdict,
+)
+
+FIXTURES = Path(__file__).parent / "fixtures" / "uk_replay"
+VENDORED_SCHEMAS = (
+    Path(__file__).resolve().parents[1]
+    / "src"
+    / "policyengine"
+    / "data"
+    / "schemas"
+    / "vendor"
+    / "policyengine-scorecard"
+)
+
+
+def sha(char: str) -> str:
+    return char * 64
+
+
+def make_pair(
+    model="2.126.8", data="1.58.0", core="3.33.0", dataset_sha=None
+) -> CandidatePair:
+    return CandidatePair(
+        country_id="uk",
+        model=PinnedWheel(name="policyengine-uk", version=model, wheel_sha256=sha("a")),
+        core=PinnedWheel(name="policyengine-core", version=core, wheel_sha256=sha("b")),
+        data=PinnedData(
+            data_package="policyengine-uk-data",
+            release_version=data,
+            build_id=f"policyengine-uk-data-{data}",
+            repo_id="policyengine/policyengine-uk-data-private",
+            repo_type="model",
+            release_manifest_path=f"releases/{data}/release_manifest.json",
+            release_manifest_revision="f" * 40,
+            release_manifest_sha256=sha("c"),
+            dataset="enhanced_frs_2024_25",
+            dataset_path="enhanced_frs_2024_25.h5",
+            dataset_revision=data,
+            dataset_sha256=dataset_sha or sha("d"),
+            size_bytes=128369110,
+        ),
+    )
+
+
+HARNESS = QualificationHarness(
+    policyengine_version="6.2.6",
+    git_commit="e" * 40,
+    packaged_bundle_manifest_sha256=sha("9"),
+)
+ISSUED = IssuedBy(
+    repository="PolicyEngine/policyengine.py",
+    workflow_run_id=1,
+    run_url="https://github.com/PolicyEngine/policyengine.py/actions/runs/1",
+)
+
+
+def gates(status="pass", **overrides) -> list[GateResult]:
+    return [
+        GateResult(
+            gate_id=gate_id, status=overrides.get(gate_id, status), summary=gate_id
+        )
+        for gate_id in REQUIRED_GATES
+    ]
+
+
+def make_receipt(
+    pair=None, gate_results=None, completed_at="2026-10-15T09:00:00Z", review=None
+) -> QualificationReceipt:
+    pair = pair or make_pair()
+    gate_results = gate_results if gate_results is not None else gates()
+    return QualificationReceipt(
+        schema_version=1,
+        receipt_type="policyengine.candidate_qualification_receipt",
+        pair_id=pair.pair_id,
+        pair=pair,
+        harness=HARNESS,
+        gates=gate_results,
+        verdict=qualification_verdict(gate_results),
+        review=review,
+        issued_by=ISSUED,
+        completed_at=completed_at,
+    )
+
+
+def golden(name: str) -> dict:
+    return json.loads((FIXTURES / f"{name}.json").read_text(encoding="utf-8"))
diff --git a/tests/test_qualification_contracts.py b/tests/test_qualification_contracts.py
new file mode 100644
index 00000000..642d6747
--- /dev/null
+++ b/tests/test_qualification_contracts.py
@@ -0,0 +1,717 @@
+"""Candidate qualification contracts, promotion order and the replay interface.
+
+Invariants:
+
+- the verdict is a pure, monotone function of the gate results: making any
+  gate worse never makes the verdict better, and a missing gate is never
+  qualified;
+- a review acceptance is bound to the exact receipt it accepts;
+- promotion picks the newest promotable pair that strictly advances the
+  certified pair, whatever order receipts arrive in, and never moves
+  backwards;
+- the replay request, status and receipt mirrors agree with the scorecard's
+  vendored JSON Schemas and reproduce its golden digests (differential);
+- a replay receipt counts only when bound to this request and these
+  artifacts; blocking flags fail, review flags need review.
+"""
+
+from __future__ import annotations
+
+import copy
+import json
+
+import jsonschema
+import pytest
+from hypothesis import given, settings
+from hypothesis import strategies as st
+from pydantic import ValidationError
+
+from policyengine.provenance.canonical import canonical_sha256
+from policyengine.provenance.qualification import (
+    REQUIRED_GATES,
+    GateResult,
+    ReplayQualificationRequest,
+    ReplayReceipt,
+    ReplayStatus,
+    ReviewAcceptance,
+    dominates,
+    evaluate_replay_receipt,
+    qualification_verdict,
+    select_promotion_target,
+)
+from tests.qualification_fixtures import (
+    FIXTURES,
+    VENDORED_SCHEMAS,
+    gates,
+    golden,
+    make_pair,
+    make_receipt,
+    sha,
+)
+
+STATUSES = ["pass", "needs_review", "unavailable", "fail", "error"]
+SEVERITY = {"pass": 0, "needs_review": 1, "unavailable": 2, "fail": 3, "error": 3}
+VERDICT_RANK = {"qualified": 0, "needs_review": 1, "incomplete": 2, "rejected": 3}
+
+
+# --- Verdict ------------------------------------------------------------------
+
+
+@pytest.mark.parametrize(
+    ("overrides", "verdict"),
+    [
+        ({}, "qualified"),
+        ({"replay": "needs_review"}, "needs_review"),
+        ({"reform_contract": "unavailable"}, "incomplete"),
+        ({"metrics_review": "unavailable", "replay": "needs_review"}, "incomplete"),
+        ({"wheel_hashes": "fail", "replay": "unavailable"}, "rejected"),
+        ({"budget_inputs": "error"}, "rejected"),
+    ],
+)
+def test_verdict_examples(overrides, verdict):
+    assert qualification_verdict(gates(**overrides)) == verdict
+
+
+def test_a_missing_required_gate_is_never_qualified():
+    for gate_id in REQUIRED_GATES:
+        partial = [gate for gate in gates() if gate.gate_id != gate_id]
+        assert qualification_verdict(partial) == "incomplete"
+
+
+@settings(max_examples=300, deadline=None)
+@given(
+    statuses=st.lists(
+        st.sampled_from(STATUSES),
+        min_size=len(REQUIRED_GATES),
+        max_size=len(REQUIRED_GATES),
+    ),
+    index=st.integers(0, len(REQUIRED_GATES) - 1),
+    worse=st.sampled_from(STATUSES),
+)
+def test_worsening_a_gate_never_improves_the_verdict(statuses, index, worse):
+    before = [
+        GateResult(gate_id=gate_id, status=status, summary="x")
+        for gate_id, status in zip(REQUIRED_GATES, statuses)
+    ]
+    if SEVERITY[worse] < SEVERITY[statuses[index]]:
+        return
+    after = list(before)
+    after[index] = GateResult(gate_id=REQUIRED_GATES[index], status=worse, summary="x")
+    assert (
+        VERDICT_RANK[qualification_verdict(after)]
+        >= VERDICT_RANK[qualification_verdict(before)]
+    )
+
+
+def test_receipt_verdict_must_follow_from_gates():
+    receipt = make_receipt(gate_results=gates(replay="needs_review"))
+    payload = receipt.model_dump(mode="json")
+    payload["verdict"] = "qualified"
+    with pytest.raises(ValidationError):
+        type(receipt).model_validate(payload)
+
+
+def test_pair_id_must_be_the_pair_digest():
+    payload = make_receipt().model_dump(mode="json")
+    payload["pair_id"] = sha("0")
+    with pytest.raises(ValidationError):
+        type(make_receipt()).model_validate(payload)
+
+
+# --- Review acceptance -----------------------------------------------------------
+
+
+def _accepted(receipt, digest=None):
+    return receipt.model_copy(
+        update={
+            "review": ReviewAcceptance(
+                reviewer="vahid",
+                accepted_at="2026-10-16T10:00:00Z",
+                accepted_receipt_sha256=digest or receipt.unreviewed_sha256,
+                explanation="UC moves follow the documented #2242 uplift.",
+            )
+        }
+    )
+
+
+def test_only_the_accepted_receipt_is_promotable():
+    receipt = make_receipt(gate_results=gates(replay="needs_review"))
+    assert not receipt.promotable
+    assert _accepted(receipt).promotable
+    assert not _accepted(receipt, digest=sha("1")).promotable
+    changed = receipt.model_copy(update={"completed_at": "2026-10-17T00:00:00Z"})
+    stale_acceptance = _accepted(changed, digest=receipt.unreviewed_sha256)
+    assert not stale_acceptance.promotable
+
+
+def test_review_cannot_attach_to_a_rejected_receipt():
+    receipt = make_receipt(gate_results=gates(wheel_hashes="fail"))
+    with pytest.raises(ValidationError):
+        type(receipt).model_validate(
+            {
+                **receipt.model_dump(mode="json"),
+                "review": _accepted(
+                    make_receipt(gate_results=gates(replay="needs_review"))
+                ).review.model_dump(),
+            }
+        )
+
+
+# --- Promotion order --------------------------------------------------------------
+
+VERSIONS = st.tuples(
+    st.sampled_from(["2.125.1", "2.126.6", "2.126.8", "2.127.0"]),
+    st.sampled_from(["1.58.0", "1.59.0", "1.60.0"]),
+    st.sampled_from(["3.33.0", "3.33.1"]),
+)
+
+
+@settings(max_examples=200, deadline=None)
+@given(a=VERSIONS, b=VERSIONS, c=VERSIONS)
+def test_dominance_is_a_strict_partial_order(a, b, c):
+    assert not dominates(a, a)
+    assert not (dominates(a, b) and dominates(b, a))
+    if dominates(a, b) and dominates(b, c):
+        assert dominates(a, c)
+
+
+RECEIPT_SPECS = st.lists(
+    st.tuples(
+        VERSIONS, st.sampled_from(["pass", "needs_review", "fail", "unavailable"])
+    ),
+    min_size=0,
+    max_size=6,
+)
+
+
+def _receipts(specs):
+    receipts = []
+    for index, ((model, data, core), status) in enumerate(specs):
+        receipts.append(
+            make_receipt(
+                pair=make_pair(model=model, data=data, core=core),
+                gate_results=gates(replay=status),
+                completed_at=f"2026-10-15T09:00:{index:02d}Z",
+            )
+        )
+    return receipts
+
+
+@settings(max_examples=200, deadline=None)
+@given(specs=RECEIPT_SPECS, current=VERSIONS, data=st.data())
+def test_promotion_target_is_order_independent_and_never_backwards(
+    specs, current, data
+):
+    receipts = _receipts(specs)
+    shuffled = data.draw(st.permutations(receipts))
+    target = select_promotion_target(receipts, current)
+    assert select_promotion_target(shuffled, current) == target
+    if target is None:
+        return
+    assert target.promotable
+    assert dominates(target.pair.order_key, current)
+    for other in receipts:
+        if other.promotable and dominates(other.pair.order_key, current):
+            key = other.pair.order_key
+            assert key == target.pair.order_key or dominates(target.pair.order_key, key)
+
+
+def test_a_stale_completion_cannot_displace_a_newer_accepted_candidate():
+    newer = make_receipt(
+        pair=make_pair(model="2.126.8"), completed_at="2026-10-15T09:00:00Z"
+    )
+    stale = make_receipt(
+        pair=make_pair(model="2.126.6"), completed_at="2026-10-15T12:00:00Z"
+    )
+    current = ("2.125.1", "1.58.0", "3.33.0")
+    assert select_promotion_target([newer, stale], current) == newer
+    assert select_promotion_target([stale, newer], current) == newer
+
+
+def test_incomparable_newest_pairs_promote_nothing_automatically():
+    newer_model = make_receipt(pair=make_pair(model="2.127.0", data="1.58.0"))
+    newer_data = make_receipt(pair=make_pair(model="2.126.8", data="1.59.0"))
+    assert (
+        select_promotion_target(
+            [newer_model, newer_data], ("2.125.1", "1.58.0", "3.33.0")
+        )
+        is None
+    )
+
+
+# --- Replay interface: mirrors agree with the vendored schemas ---------------
+
+
+def _validator(name):
+    schema = json.loads((VENDORED_SCHEMAS / name).read_text())
+    return jsonschema.Draft202012Validator(
+        schema, format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER
+    )
+
+
+def test_golden_request_round_trips_with_its_digest():
+    raw = golden("qualification_request_golden")
+    request = ReplayQualificationRequest.model_validate(raw)
+    assert request.document() == raw
+    digests = json.loads((FIXTURES / "DIGESTS.json").read_text())
+    assert request.request_id == canonical_sha256(raw)
+    assert (
+        request.request_id == digests["qualification_request_golden.json"]["request_id"]
+    )
+    _validator("qualification_request.v1.json").validate(request.document())
+
+
+def test_golden_receipt_digest_matches_received_bytes():
+    raw = golden("replay_receipt_golden")
+    receipt = ReplayReceipt.from_document(raw)
+    assert receipt.computed_receipt_sha256 == raw["receipt_sha256"]
+    _validator("replay_receipt.v1.json").validate(raw)
+
+
+REQUEST_MUTATIONS = [
+    ("extra member", lambda r: r.update(unexpected=True)),
+    ("bad kind", lambda r: r.update(kind="other")),
+    ("attempt zero", lambda r: r.update(attempt=0)),
+    ("bad dataset digest", lambda r: r["candidate"]["data"].update(sha256="xyz")),
+    ("bad repo type", lambda r: r["candidate"]["data"].update(repo_type="space")),
+    (
+        "wrapper with both sources",
+        lambda r: r["candidate"]["policyengine"]["source"].update(
+            pypi={"wheel_sha256": "a" * 64}
+        ),
+    ),
+    ("released bundle from git", lambda r: r.update(kind="released_bundle")),
+    (
+        "missing bundle field",
+        lambda r: r["candidate"]["expected_release_bundle"].pop("model_version"),
+    ),
+    ("bad commit", lambda r: r["requested_by"].update(commit="abc")),
+    ("missing commit", lambda r: r["requested_by"].pop("commit")),
+]
+
+
+@pytest.mark.parametrize(
+    ("label", "mutate"), REQUEST_MUTATIONS, ids=[m[0] for m in REQUEST_MUTATIONS]
+)
+def test_request_mirror_and_schema_reject_the_same_requests(label, mutate):
+    raw = copy.deepcopy(golden("qualification_request_golden"))
+    mutate(raw)
+    assert not _validator("qualification_request.v1.json").is_valid(raw), label
+    with pytest.raises(ValidationError):
+        ReplayQualificationRequest.model_validate(raw)
+
+
+REQUEST_MUTATIONS_V1_TIGHTENED = [
+    ("parent segment", "../enhanced_frs_2024_25.h5"),
+    ("nested parent", "a/../b.h5"),
+    ("trailing parent", "a/.."),
+]
+
+
+@pytest.mark.parametrize(
+    ("label", "path"),
+    REQUEST_MUTATIONS_V1_TIGHTENED,
+    ids=[m[0] for m in REQUEST_MUTATIONS_V1_TIGHTENED],
+)
+def test_request_mirror_and_schema_refuse_parent_segments(label, path):
+    raw = copy.deepcopy(golden("qualification_request_golden"))
+    raw["candidate"]["data"]["path"] = path
+    assert not _validator("qualification_request.v1.json").is_valid(raw), label
+    with pytest.raises(ValidationError):
+        ReplayQualificationRequest.model_validate(raw)
+
+
+def test_request_mirror_and_schema_accept_dotted_names():
+    raw = copy.deepcopy(golden("qualification_request_golden"))
+    raw["candidate"]["data"]["path"] = "releases/1.58.0/..hidden.h5"
+    _validator("qualification_request.v1.json").validate(raw)
+    ReplayQualificationRequest.model_validate(raw)
+
+
+def _rejected_receipt():
+    doc = copy.deepcopy(golden("replay_receipt_golden"))
+    doc.update(
+        status="rejected",
+        rejection={"code": "artifact_hash_mismatch", "detail": "dataset bytes differ"},
+        corpus=None,
+        diff=None,
+        variant_id=None,
+        variant_identity_sha256=None,
+        execution=None,
+    )
+    doc["integrity"] = {"status": "not_run", "failures": []}
+    return _sealed(doc)
+
+
+RECEIPT_MUTATIONS = [
+    (
+        "complete without manifest",
+        "complete",
+        lambda r: r["results"].pop("manifest_sha256"),
+    ),
+    (
+        "complete without identity",
+        "complete",
+        lambda r: r.update(variant_identity_sha256=None),
+    ),
+    (
+        "unanchored python",
+        "complete",
+        lambda r: r["execution"].update(python="3.12.14+local"),
+    ),
+    ("rejected with variant", "rejected", lambda r: r.update(variant_id="pe6.2.6")),
+    (
+        "rejected with identity",
+        "rejected",
+        lambda r: r.update(variant_identity_sha256="0" * 64),
+    ),
+    (
+        "rejected with integrity",
+        "rejected",
+        lambda r: r.update(integrity={"status": "failed", "failures": []}),
+    ),
+]
+
+
+@pytest.mark.parametrize(
+    ("label", "base", "mutate"),
+    RECEIPT_MUTATIONS,
+    ids=[m[0] for m in RECEIPT_MUTATIONS],
+)
+def test_receipt_mirror_and_schema_reject_the_same_receipts(label, base, mutate):
+    """Differential against W6's tightened replay_receipt.v1.json (dc040de)."""
+
+    raw = (
+        copy.deepcopy(golden("replay_receipt_golden"))
+        if base == "complete"
+        else _rejected_receipt()
+    )
+    _validator("replay_receipt.v1.json").validate(raw)
+    ReplayReceipt.from_document(raw)
+    mutate(raw)
+    raw = _sealed(raw)
+    assert not _validator("replay_receipt.v1.json").is_valid(raw), label
+    with pytest.raises(ValidationError):
+        ReplayReceipt.from_document(raw)
+
+
+def test_requested_by_is_open_as_in_the_schema():
+    raw = copy.deepcopy(golden("qualification_request_golden"))
+    raw["requested_by"]["pair_id"] = sha("a")
+    request = ReplayQualificationRequest.model_validate(raw)
+    assert request.document()["requested_by"]["pair_id"] == sha("a")
+    _validator("qualification_request.v1.json").validate(request.document())
+
+
+# --- Replay gate ------------------------------------------------------------------
+
+
+def _sealed(receipt_doc):
+    doc = {key: value for key, value in receipt_doc.items() if key != "receipt_sha256"}
+    doc["receipt_sha256"] = canonical_sha256(doc)
+    return doc
+
+
+def _status(request_id, receipt_doc, status=None):
+    return ReplayStatus.model_validate(
+        {
+            "schema": "policyengine.uk-replay-status/v1",
+            "request_id": request_id,
+            "status": status or receipt_doc["status"],
+            "history": [
+                {
+                    "status": status or receipt_doc["status"],
+                    "at": "2026-10-15T10:00:00Z",
+                    "run_url": "https://github.com/PolicyEngine/policyengine-scorecard/actions/runs/7",
+                }
+            ],
+            "receipt_sha256": receipt_doc["receipt_sha256"],
+            "receipt_commit": "c" * 40,
+        }
+    )
+
+
+def _gate(receipt_doc, request=None, status=None):
+    request = request or ReplayQualificationRequest.model_validate(
+        golden("qualification_request_golden")
+    )
+    receipt = ReplayReceipt.from_document(receipt_doc)
+    return evaluate_replay_receipt(
+        request, status or _status(request.request_id, receipt_doc), receipt
+    )
+
+
+def _with_requested_wheels(doc):
+    """Add artifact evidence omitted by the scorecard's synthetic golden fixture."""
+
+    requested = golden("qualification_request_golden")["candidate"]
+    for package, request_key in (
+        ("policyengine-uk", "policyengine_uk"),
+        ("policyengine-core", "policyengine_core"),
+    ):
+        doc["execution"]["packages"][package]["wheel_sha256"] = requested[request_key][
+            "wheel_sha256"
+        ]
+    return doc
+
+
+def _with_counts(sealed=True, **counts):
+    """The golden receipt with these flag counts and one flagged row per flag."""
+
+    doc = _with_requested_wheels(copy.deepcopy(golden("replay_receipt_golden")))
+    base = {key: 0 for key in doc["diff"]["flag_counts"]}
+    base.update(counts)
+    doc["diff"]["flag_counts"] = base
+    template = doc["diff"]["flagged_rows"][0]
+    doc["diff"]["flagged_rows"] = [
+        {**template, "flags": [kind], "year": 2024 + index}
+        for kind, count in counts.items()
+        for index in range(count)
+    ]
+    return _sealed(doc) if sealed else doc
+
+
+def test_golden_receipt_fails_on_its_structural_flag():
+    doc = _with_requested_wheels(copy.deepcopy(golden("replay_receipt_golden")))
+    gate = _gate(_sealed(doc))
+    assert gate.status == "fail"
+    assert "structural" in gate.summary
+
+
+@pytest.mark.parametrize(
+    ("counts", "status"),
+    [
+        ({}, "pass"),
+        ({"numerical": 2}, "needs_review"),
+        ({"support": 1}, "needs_review"),
+        ({"numerical": 2, "coverage": 1}, "fail"),
+        ({"construction": 1}, "fail"),
+        ({"inertness": 1}, "fail"),
+        ({"mapping": 3}, "fail"),
+    ],
+)
+def test_flag_policy(counts, status):
+    assert _gate(_with_counts(**counts)).status == status
+
+
+def _clean_receipt():
+    return _with_counts(sealed=False)
+
+
+def _edit(receipt, edit):
+    edit(receipt)
+    return _sealed(receipt)
+
+
+EVENT = "spring_budget_2023"
+
+CONTRADICTIONS = {
+    "a structural row with zero counts": lambda r: r["diff"].update(
+        flagged_rows=[{**golden("replay_receipt_golden")["diff"]["flagged_rows"][1]}]
+    ),
+    "a failure under complete integrity": lambda r: r["integrity"].update(
+        failures=[{"code": "construction", "detail": "measure failed"}]
+    ),
+    "supported years with no artifacts": lambda r: r["corpus"]["events"][EVENT].update(
+        artifacts=0
+    ),
+    "nothing replayed": lambda r: r["corpus"]["events"][EVENT].update(
+        artifacts=0, supported_years=[]
+    ),
+    "an event with no years": lambda r: r["corpus"]["events"][EVENT].update(
+        supported_years=[], unsupported_years=[]
+    ),
+}
+
+
+@pytest.mark.parametrize("name", sorted(CONTRADICTIONS))
+def test_a_receipt_that_contradicts_itself_fails(name):
+    """Interrupted review of #574: each of these was schema-valid, correctly
+    digested, and passed the gate because the summary counters were zero."""
+
+    receipt = _edit(_clean_receipt(), CONTRADICTIONS[name])
+    _validator("replay_receipt.v1.json").validate(receipt)
+    gate = _gate(receipt)
+    assert gate.status == "fail", (name, gate.summary)
+    assert "contradicts itself or the request" in gate.summary
+
+
+def test_blocking_flags_count_wherever_the_receipt_states_them():
+    """A row's flag blocks even when its counter is understated."""
+
+    receipt = _with_counts(sealed=False, numerical=1)
+    receipt["diff"]["flagged_rows"].append(
+        {**receipt["diff"]["flagged_rows"][0], "flags": ["mapping"], "year": 2030}
+    )
+    receipt["diff"]["flag_counts"]["mapping"] = 1
+    assert _gate(_sealed(receipt)).status == "fail"
+    assert "mapping" in _gate(_sealed(receipt)).summary
+
+
+def test_an_inert_measure_year_without_a_flag_needs_review():
+    receipt = _edit(
+        _clean_receipt(),
+        lambda r: r["corpus"]["events"][EVENT].update(
+            inert_measure_years=[{"measure_key": "uc_childcare", "year": 2026}]
+        ),
+    )
+    gate = _gate(receipt)
+    assert gate.status == "needs_review" and "inert" in gate.summary
+
+
+def test_the_comparison_base_must_be_the_one_requested():
+    raw = copy.deepcopy(golden("qualification_request_golden"))
+    base_variant = _clean_receipt()["diff"]["base_variant_id"]
+    raw["compare_to"] = base_variant
+    explicit = ReplayQualificationRequest.model_validate(raw)
+    receipt = _clean_receipt()
+    receipt["request_id"] = explicit.request_id
+    assert _gate(_sealed(receipt), request=explicit).status == "pass"
+    raw["compare_to"] = "pe6.2.6_uk2.125.1_policyengine-uk-data-1.58.0"
+    other = ReplayQualificationRequest.model_validate(raw)
+    receipt = _clean_receipt()
+    receipt["request_id"] = other.request_id
+    gate = _gate(_sealed(receipt), request=other)
+    assert gate.status == "fail" and "asked for a comparison" in gate.summary
+
+
+def test_incomplete_or_failed_integrity_fails():
+    doc = copy.deepcopy(golden("replay_receipt_golden"))
+    doc["diff"]["flag_counts"] = {key: 0 for key in doc["diff"]["flag_counts"]}
+    doc["integrity"]["status"] = "incomplete"
+    assert _gate(_sealed(doc)).status == "fail"
+    doc["integrity"] = {
+        "status": "failed",
+        "failures": [{"code": "non_finite", "detail": "NaN in VAT"}],
+    }
+    doc["status"] = "failed"
+    assert _gate(_sealed(doc)).status == "fail"
+
+
+def test_execution_of_other_artifacts_fails():
+    for mutate in (
+        lambda d: d["execution"].update(dataset_sha256="0" * 64),
+        lambda d: d["execution"].update(packaged_bundle_manifest_sha256="0" * 64),
+        lambda d: d["execution"]["packages"]["policyengine-uk"].update(version="9.9.9"),
+        lambda d: d["execution"]["release_bundle"].update(model_version="9.9.9"),
+    ):
+        doc = copy.deepcopy(_with_counts())
+        mutate(doc)
+        assert _gate(_sealed(doc)).status == "fail"
+
+
+@pytest.mark.parametrize("package", ["policyengine-uk", "policyengine-core"])
+@pytest.mark.parametrize("wheel_sha256", [None, "0" * 64])
+def test_replay_requires_exact_country_and_core_wheels(package, wheel_sha256):
+    doc = _clean_receipt()
+    installed = doc["execution"]["packages"][package]
+    if wheel_sha256 is None:
+        installed.pop("wheel_sha256")
+    else:
+        installed["wheel_sha256"] = wheel_sha256
+    gate = _gate(_sealed(doc))
+    assert gate.status == "fail"
+    assert f"{package} wheel hash" in gate.summary
+
+
+@pytest.mark.parametrize("wheel_sha256", [None, "0" * 64])
+def test_released_bundle_replay_requires_requested_wrapper_wheel(wheel_sha256):
+    raw = copy.deepcopy(golden("qualification_request_golden"))
+    raw["kind"] = "released_bundle"
+    raw["candidate"]["policyengine"]["source"] = {"pypi": {"wheel_sha256": "a" * 64}}
+    request = ReplayQualificationRequest.model_validate(raw)
+    doc = _clean_receipt()
+    doc["request_id"] = request.request_id
+    if wheel_sha256 is not None:
+        doc["execution"]["packages"]["policyengine"]["wheel_sha256"] = wheel_sha256
+    gate = _gate(_sealed(doc), request=request)
+    assert gate.status == "fail"
+    assert "policyengine wheel hash" in gate.summary
+
+
+def test_released_bundle_replay_accepts_exact_requested_wrapper_wheel():
+    raw = copy.deepcopy(golden("qualification_request_golden"))
+    raw["kind"] = "released_bundle"
+    raw["candidate"]["policyengine"]["source"] = {"pypi": {"wheel_sha256": "a" * 64}}
+    request = ReplayQualificationRequest.model_validate(raw)
+    doc = _clean_receipt()
+    doc["request_id"] = request.request_id
+    doc["execution"]["packages"]["policyengine"]["wheel_sha256"] = "a" * 64
+    assert _gate(_sealed(doc), request=request).status == "pass"
+
+
+def test_unbound_receipts_fail():
+    sealed = _with_counts()
+    tampered = copy.deepcopy(sealed)
+    tampered["diff"]["flag_counts"]["numerical"] = 0
+    tampered["corpus"]["corpus_version"] = "tampered"
+    assert _gate(tampered).status == "fail"  # digest no longer matches the bytes
+    request = ReplayQualificationRequest.model_validate(
+        golden("qualification_request_golden")
+    )
+    other = _status("0" * 64, sealed)
+    assert _gate(sealed, request=request, status=other).status == "fail"
+
+
+def test_rejected_receipt_fails_with_its_code():
+    doc = copy.deepcopy(golden("replay_receipt_golden"))
+    doc.update(
+        status="rejected",
+        rejection={"code": "artifact_hash_mismatch", "detail": "dataset bytes differ"},
+        corpus=None,
+        diff=None,
+        variant_id=None,
+        variant_identity_sha256=None,
+        execution=None,
+    )
+    doc["integrity"] = {"status": "not_run", "failures": []}
+    gate = _gate(_sealed(doc))
+    assert gate.status == "fail"
+    assert "artifact_hash_mismatch" in gate.summary
+
+
+def test_status_must_name_terminal_receipt():
+    with pytest.raises(ValidationError):
+        ReplayStatus.model_validate(
+            {
+                "schema": "policyengine.uk-replay-status/v1",
+                "request_id": "a" * 64,
+                "status": "complete",
+                "history": [
+                    {
+                        "status": "complete",
+                        "at": "2026-10-15T10:00:00Z",
+                        "run_url": "https://github.com/PolicyEngine/policyengine-scorecard/actions/runs/7",
+                    }
+                ],
+            }
+        )
+
+
+def test_requests_without_a_run_id_are_valid_as_in_the_schema():
+    raw = copy.deepcopy(golden("qualification_request_golden"))
+    raw["requested_by"].pop("workflow_run_id")
+    _validator("qualification_request.v1.json").validate(raw)
+    assert ReplayQualificationRequest.model_validate(raw).document() == raw
+
+
+def test_early_failure_receipt_fails_the_gate():
+    doc = copy.deepcopy(golden("replay_receipt_golden"))
+    doc.update(
+        status="failed",
+        variant_id=None,
+        variant_identity_sha256=None,
+        execution=None,
+        corpus=None,
+        diff=None,
+    )
+    doc["integrity"] = {
+        "status": "failed",
+        "failures": [
+            {"code": "wheel_download_failed", "detail": "pip download failed"}
+        ],
+    }
+    sealed = _sealed(doc)
+    _validator("replay_receipt.v1.json").validate(sealed)
+    gate = _gate(sealed)
+    assert gate.status == "fail" and "wheel_download_failed" in gate.summary
diff --git a/tests/test_qualification_ledger_concurrency.py b/tests/test_qualification_ledger_concurrency.py
new file mode 100644
index 00000000..3ca61a03
--- /dev/null
+++ b/tests/test_qualification_ledger_concurrency.py
@@ -0,0 +1,515 @@
+"""Concurrent publishes never yield a receipt that mixes two attempts.
+
+Round-6 review of #574: two attempts of one pair both write the pair's
+published files. When the older attempt's replay finished after the newer
+attempt had published, the publish script's cherry-pick merged the two
+indented JSON receipts line by line into a valid-looking receipt: the newer
+attempt's harness with the older attempt's passing replay. Promotion then
+readied a commit whose replay never ran.
+
+Invariants:
+
+- after any interleaving of two publishes, the pair's published receipt is
+  the current receipt of its latest-staged attempt, and every attempt's
+  evidence is on the branch;
+- without a rederive hook the publish fails rather than merging receipts;
+- a published receipt that is not its latest attempt's own is refused by
+  every reader (listing, promotion, acceptance, remote fetch).
+"""
+
+from __future__ import annotations
+
+import json
+import os
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+from types import SimpleNamespace
+
+import pytest
+
+from policyengine.provenance.canonical import canonical_sha256
+from policyengine.provenance.qualification import (
+    QualificationReceipt,
+    ReplayReceipt,
+    ReplayStatus,
+    ReviewAcceptance,
+    evaluate_replay_receipt,
+)
+from policyengine.provenance.qualification_ledger import (
+    LedgerInconsistency,
+    fetch_ledger_receipt,
+    latest_receipts,
+    published_receipt,
+    read_json,
+    rederive_publication,
+    write_json,
+)
+from policyengine.provenance.uk_qualification import (
+    accept_review,
+    build_replay_request,
+    candidate_branch,
+    finalize_attempt,
+    plan_promotion,
+    previous_certificate,
+    stage_attempt,
+    write_gate_results,
+)
+from tests.qualification_fixtures import HARNESS, ISSUED, gates, golden, make_pair
+
+ROOT = Path(__file__).resolve().parents[1]
+SCRIPT = ROOT / ".github" / "qualification-ledger.sh"
+CLI = ROOT / "scripts" / "check_uk_candidate.py"
+BASE = json.loads(
+    (ROOT / "src" / "policyengine" / "data" / "bundle" / "manifest.json").read_text()
+)
+PAIR = make_pair(
+    model="2.200.0", data=BASE["data_releases"]["uk"]["data_package"]["version"]
+)
+NEWER_HARNESS = HARNESS.model_copy(
+    update={"git_commit": "b" * 40, "packaged_bundle_manifest_sha256": "8" * 64}
+)
+REDERIVE = f"{sys.executable} {CLI} rederive-ledger"
+
+pytestmark = pytest.mark.skipif(shutil.which("git") is None, reason="needs git")
+
+
+def _run(*args, cwd, check=True, rederive=None):
+    env = {
+        **os.environ,
+        "GIT_AUTHOR_NAME": "test",
+        "GIT_AUTHOR_EMAIL": "test@example.org",
+        "GIT_COMMITTER_NAME": "test",
+        "GIT_COMMITTER_EMAIL": "test@example.org",
+        "GIT_TERMINAL_PROMPT": "0",
+        "LEDGER_RETRY_DELAY": "0",
+        "POLICYENGINE_SKIP_COUNTRY_IMPORTS": "1",
+        "PYTHONPATH": str(ROOT / "src") + os.pathsep + str(ROOT),
+    }
+    env.pop("LEDGER_REDERIVE", None)
+    if rederive is not None:
+        env["LEDGER_REDERIVE"] = rederive
+    return subprocess.run(
+        args,
+        cwd=cwd,
+        env=env,
+        check=check,
+        text=True,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+    )
+
+
+def _request(harness):
+    return build_replay_request(
+        PAIR,
+        harness,
+        release_bundle=golden("qualification_request_golden")["candidate"][
+            "expected_release_bundle"
+        ],
+        issued_by=ISSUED,
+    )
+
+
+def _complete_replay(attempt: Path, request) -> None:
+    """Write a passing replay for ``request`` into ``attempt``."""
+
+    document = golden("replay_receipt_golden")
+    document["request_id"] = request.request_id
+    execution = document["execution"]
+    execution["packaged_bundle_manifest_sha256"] = (
+        request.candidate.policyengine.packaged_bundle_manifest_sha256
+    )
+    execution["dataset_sha256"] = PAIR.data.dataset_sha256
+    execution["release_bundle"] = request.candidate.expected_release_bundle
+    execution["packages"]["policyengine"]["version"] = (
+        request.candidate.policyengine.version
+    )
+    for key, package in (
+        ("policyengine-uk", PAIR.model),
+        ("policyengine-core", PAIR.core),
+    ):
+        execution["packages"][key]["version"] = package.version
+        execution["packages"][key]["wheel_sha256"] = package.wheel_sha256
+    document["diff"]["flag_counts"] = {
+        key: 0 for key in document["diff"]["flag_counts"]
+    }
+    document["diff"]["flagged_rows"] = []
+    document["receipt_sha256"] = canonical_sha256(
+        {key: value for key, value in document.items() if key != "receipt_sha256"}
+    )
+    receipt = ReplayReceipt.from_document(document)
+    status = ReplayStatus.model_validate(
+        {
+            "schema": "policyengine.uk-replay-status/v1",
+            "request_id": request.request_id,
+            "status": "complete",
+            "receipt_sha256": receipt.receipt_sha256,
+            "receipt_commit": "c" * 40,
+            "history": [
+                {
+                    "status": "complete",
+                    "at": "2026-10-15T10:00:00Z",
+                    "run_url": "https://github.com/PolicyEngine/policyengine-scorecard/actions/runs/7",
+                }
+            ],
+        }
+    )
+    gate = evaluate_replay_receipt(request, status, receipt)
+    assert gate.status == "pass", gate
+    write_json(attempt / "replay" / "request.json", request.document())
+    write_json(attempt / "replay" / "status.json", status.document())
+    write_json(attempt / "replay" / "receipt.json", receipt.document())
+    write_json(attempt / "replay" / "replay.result.json", gate.canonical_payload())
+
+
+def _stage(attempt: Path, harness, owner: str, staged_at: str) -> None:
+    payload = {
+        "pair": PAIR.canonical_payload(),
+        "previous_certificate": previous_certificate(BASE).canonical_payload(),
+        "branch": candidate_branch(PAIR),
+    }
+    stage_attempt(attempt, payload, harness, owner_run_id=owner, staged_at=staged_at)
+    write_gate_results(attempt, [g for g in gates() if g.gate_id != "replay"], {})
+
+
+def _origin(root: Path) -> Path:
+    origin = root / "origin.git"
+    _run("git", "init", "--quiet", "--bare", "-b", "main", str(origin), cwd=root)
+    seed = root / "seed"
+    _run("git", "clone", "--quiet", str(origin), str(seed), cwd=root)
+    (seed / "seed").write_text("seed")
+    _run("git", "add", "seed", cwd=seed)
+    _run("git", "commit", "--quiet", "-m", "seed", cwd=seed)
+    _run("git", "push", "--quiet", "origin", "HEAD:refs/heads/main", cwd=seed)
+    return origin
+
+
+def _ledger_clone(root: Path, origin: Path, name: str) -> Path:
+    clone = root / name
+    _run(
+        "git", "clone", "--quiet", "--branch", "main", str(origin), str(clone), cwd=root
+    )
+    _run("bash", str(SCRIPT), "checkout", "ledger", cwd=clone)
+    return clone
+
+
+def _published(origin: Path, path: str) -> dict:
+    shown = _run(
+        "git", "show", f"uk-qualification-ledger:uk/{PAIR.pair_id}/{path}", cwd=origin
+    )
+    return json.loads(shown.stdout)
+
+
+def _race(tmp_path: Path, *, older_publishes_last: bool, rederive):
+    """Attempt A (older) and B (newer) of one pair publish concurrently.
+
+    A is staged and published first. Each run then works in its own clone:
+    B finalizes incomplete; A's replay completes and it finalizes qualified.
+    """
+
+    origin = _origin(tmp_path)
+    seed = _ledger_clone(tmp_path, origin, "seed-ledger")
+    pair_dir = seed / "ledger" / "uk" / PAIR.pair_id
+    _stage(pair_dir / "attempts" / "a", HARNESS, "1", "2026-10-15T09:00:00Z")
+    finalize_attempt(
+        pair_dir / "attempts" / "a",
+        seed / "ledger",
+        issued_by=ISSUED,
+        completed_at="2026-10-15T09:30:00Z",
+    )
+    _run("bash", str(SCRIPT), "publish", "ledger", "A incomplete", cwd=seed)
+
+    clone_a = _ledger_clone(tmp_path, origin, "a")
+    clone_b = _ledger_clone(tmp_path, origin, "b")
+    attempt_a = clone_a / "ledger" / "uk" / PAIR.pair_id / "attempts" / "a"
+    attempt_b = clone_b / "ledger" / "uk" / PAIR.pair_id / "attempts" / "b"
+    _stage(attempt_b, NEWER_HARNESS, "2", "2026-10-15T10:01:00Z")
+    receipt_b = finalize_attempt(
+        attempt_b,
+        clone_b / "ledger",
+        issued_by=ISSUED.model_copy(update={"workflow_run_id": 2}),
+        completed_at="2026-10-15T10:02:00Z",
+    )
+    assert receipt_b.verdict == "incomplete"
+    _complete_replay(attempt_a, _request(HARNESS))
+    receipt_a = finalize_attempt(
+        attempt_a,
+        clone_a / "ledger",
+        issued_by=ISSUED,
+        completed_at="2026-10-15T10:02:00Z",
+    )
+    assert receipt_a.verdict == "qualified"
+
+    order = [(clone_b, "B"), (clone_a, "A")]
+    if not older_publishes_last:
+        order.reverse()
+    first, second = order
+    _run("bash", str(SCRIPT), "publish", "ledger", f"{first[1]}", cwd=first[0])
+    result = _run(
+        "bash",
+        str(SCRIPT),
+        "publish",
+        "ledger",
+        f"{second[1]}",
+        cwd=second[0],
+        check=False,
+        rederive=rederive,
+    )
+    return origin, result, receipt_a, receipt_b
+
+
+@pytest.mark.parametrize("older_publishes_last", [True, False])
+def test_concurrent_attempts_publish_the_latest_attempts_receipt(
+    tmp_path, older_publishes_last
+):
+    origin, result, receipt_a, receipt_b = _race(
+        tmp_path, older_publishes_last=older_publishes_last, rederive=REDERIVE
+    )
+    assert result.returncode == 0, result.stdout
+    published = QualificationReceipt.model_validate(
+        _published(origin, "qualification_receipt.json")
+    )
+    # B was staged last, so B's receipt is published, whichever came last.
+    assert published.unreviewed_sha256 == receipt_b.unreviewed_sha256
+    assert published.harness.git_commit == "b" * 40
+    assert _published(origin, "latest_attempt.json")["attempt_key"] == "b"
+    files = _run(
+        "git", "ls-tree", "-r", "--name-only", "uk-qualification-ledger", cwd=origin
+    ).stdout.split()
+    for key in ("a", "b"):
+        assert f"uk/{PAIR.pair_id}/attempts/{key}/qualification_receipt.json" in files
+    # Nothing qualified is published for B, so B's commit is never readied.
+    plan = plan_promotion([published], BASE, {candidate_branch(PAIR): "b" * 40})
+    assert plan.ready_commit is None
+
+
+def test_without_a_rederive_hook_the_publish_fails_rather_than_merging(tmp_path):
+    origin, result, _, receipt_b = _race(
+        tmp_path, older_publishes_last=True, rederive=None
+    )
+    assert result.returncode != 0
+    assert "conflicts with a concurrent publish" in result.stdout
+    published = QualificationReceipt.model_validate(
+        _published(origin, "qualification_receipt.json")
+    )
+    assert published.unreviewed_sha256 == receipt_b.unreviewed_sha256
+
+
+def _ledger_with_attempts(root: Path):
+    """A ledger pair with attempt A qualified (sequence 1) and B incomplete (2)."""
+
+    ledger = root / "ledger"
+    pair_dir = ledger / "uk" / PAIR.pair_id
+    _stage(pair_dir / "attempts" / "a", HARNESS, "1", "2026-10-15T09:00:00Z")
+    _complete_replay(pair_dir / "attempts" / "a", _request(HARNESS))
+    receipt_a = finalize_attempt(
+        pair_dir / "attempts" / "a",
+        ledger,
+        issued_by=ISSUED,
+        completed_at="2026-10-15T09:30:00Z",
+    )
+    _stage(pair_dir / "attempts" / "b", NEWER_HARNESS, "2", "2026-10-15T10:01:00Z")
+    receipt_b = finalize_attempt(
+        pair_dir / "attempts" / "b",
+        ledger,
+        issued_by=ISSUED,
+        completed_at="2026-10-15T10:02:00Z",
+    )
+    return ledger, pair_dir, receipt_a, receipt_b
+
+
+def _hybrid(receipt_a, receipt_b) -> dict:
+    """The round-6 merge: B's harness and identity with A's replay and gates."""
+
+    hybrid = receipt_a.canonical_payload()
+    hybrid["harness"] = receipt_b.harness.canonical_payload()
+    hybrid["issued_by"] = receipt_b.issued_by.canonical_payload()
+    QualificationReceipt.model_validate(hybrid)  # it is a valid-looking receipt
+    return hybrid
+
+
+def test_a_merged_receipt_is_refused_by_every_reader(tmp_path, monkeypatch):
+    ledger, pair_dir, receipt_a, receipt_b = _ledger_with_attempts(tmp_path)
+    assert published_receipt(pair_dir).unreviewed_sha256 == receipt_b.unreviewed_sha256
+    write_json(pair_dir / "qualification_receipt.json", _hybrid(receipt_a, receipt_b))
+
+    with pytest.raises(LedgerInconsistency, match="not the receipt of attempt b"):
+        published_receipt(pair_dir)
+    problems: list[str] = []
+    assert latest_receipts(ledger, problems=problems) == []
+    assert problems and PAIR.pair_id in problems[0]
+
+    import importlib.util
+
+    spec = importlib.util.spec_from_file_location("ledger_concurrency_cli", CLI)
+    cli = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(cli)
+
+    with pytest.raises(LedgerInconsistency):
+        cli.cmd_accept(
+            SimpleNamespace(
+                ledger=str(ledger),
+                pair_id=PAIR.pair_id,
+                reviewer="r",
+                receipt_sha256="0" * 64,
+                explanation="x",
+            )
+        )
+
+    assert (
+        rederive_publication(pair_dir).unreviewed_sha256 == receipt_b.unreviewed_sha256
+    )
+    assert published_receipt(pair_dir).unreviewed_sha256 == receipt_b.unreviewed_sha256
+
+
+def test_an_older_attempts_real_receipt_is_not_the_published_one(tmp_path):
+    """A stale but genuine receipt is refused too: the latest attempt decides."""
+
+    _, pair_dir, receipt_a, _ = _ledger_with_attempts(tmp_path)
+    write_json(pair_dir / "qualification_receipt.json", receipt_a.canonical_payload())
+    with pytest.raises(LedgerInconsistency):
+        published_receipt(pair_dir)
+
+
+def test_rederivation_keeps_a_review_acceptance_of_exactly_that_receipt(tmp_path):
+    from tests.qualification_fixtures import make_receipt
+
+    ledger = tmp_path / "ledger"
+    pair_dir = ledger / "uk" / PAIR.pair_id
+    _stage(pair_dir / "attempts" / "a", HARNESS, "1", "2026-10-15T09:00:00Z")
+    receipt = make_receipt(pair=PAIR, gate_results=gates(metrics_review="needs_review"))
+    assert receipt.verdict == "needs_review" and not receipt.promotable
+    write_json(
+        pair_dir / "attempts" / "a" / "qualification_receipt.json",
+        receipt.canonical_payload(),
+    )
+    accepted = accept_review(
+        receipt,
+        reviewer="reviewer",
+        accepted_receipt_sha256=receipt.unreviewed_sha256,
+        explanation="expected",
+        accepted_at="2026-10-15T11:00:00Z",
+    )
+    write_json(
+        pair_dir / "reviews" / f"{accepted.receipt_sha256}.json",
+        accepted.canonical_payload(),
+    )
+    # A published file a merge left unreadable is rewritten, keeping the review.
+    write_json(pair_dir / "qualification_receipt.json", {"not": "a receipt"})
+    rederived = rederive_publication(pair_dir)
+    assert rederived.review is not None and rederived.promotable
+    assert published_receipt(pair_dir) == rederived
+    assert read_json(pair_dir / "latest_attempt.json") == {
+        "attempt_key": "a",
+        "sequence": read_json(pair_dir / "attempts" / "a" / "staging.json")["sequence"],
+    }
+
+
+@pytest.mark.parametrize("flaw", ["of another receipt", "not a valid acceptance"])
+def test_rederivation_drops_a_review_that_does_not_accept_that_receipt(tmp_path, flaw):
+    """Offered as the published file and as a stored review, an acceptance is
+    kept only if it promotes exactly the latest attempt's receipt."""
+
+    from tests.qualification_fixtures import make_receipt
+
+    pair_dir = tmp_path / "ledger" / "uk" / PAIR.pair_id
+    _stage(pair_dir / "attempts" / "a", HARNESS, "1", "2026-10-15T09:00:00Z")
+    receipt = make_receipt(pair=PAIR, gate_results=gates(metrics_review="needs_review"))
+    write_json(
+        pair_dir / "attempts" / "a" / "qualification_receipt.json",
+        receipt.canonical_payload(),
+    )
+    if flaw == "of another receipt":
+        other = make_receipt(
+            pair=PAIR,
+            gate_results=gates(metrics_review="needs_review"),
+            completed_at="2026-10-15T10:00:00Z",
+        )
+        assert other.unreviewed_sha256 != receipt.unreviewed_sha256
+        offered = accept_review(
+            other,
+            reviewer="reviewer",
+            accepted_receipt_sha256=other.unreviewed_sha256,
+            explanation="expected",
+            accepted_at="2026-10-15T11:00:00Z",
+        )
+        assert offered.promotable
+    else:
+        offered = QualificationReceipt.model_validate(
+            {
+                **receipt.canonical_payload(),
+                "review": ReviewAcceptance(
+                    reviewer="reviewer",
+                    accepted_at="2026-10-15T11:00:00Z",
+                    accepted_receipt_sha256="0" * 64,
+                    explanation="expected",
+                ).canonical_payload(),
+            }
+        )
+        assert offered.unreviewed_sha256 == receipt.unreviewed_sha256
+        assert offered.review is not None and not offered.promotable
+    write_json(
+        pair_dir / "reviews" / f"{offered.receipt_sha256}.json",
+        offered.canonical_payload(),
+    )
+    write_json(pair_dir / "qualification_receipt.json", offered.canonical_payload())
+
+    rederived = rederive_publication(pair_dir)
+    assert rederived.review is None and not rederived.promotable
+    assert rederived.unreviewed_sha256 == receipt.unreviewed_sha256
+    assert published_receipt(pair_dir) == rederived
+
+
+class _Response:
+    def __init__(self, text=None):
+        self.status_code = 404 if text is None else 200
+        self.text = text or ""
+
+    def raise_for_status(self):
+        return None
+
+
+def _remote(files: dict):
+    def get(url, timeout):
+        # The longest matching suffix wins: an attempt's receipt URL also ends
+        # with the pair's receipt path suffix.
+        matches = [suffix for suffix in files if url.endswith(suffix)]
+        if not matches:
+            return _Response(None)
+        value = files[max(matches, key=len)]
+        return _Response(None if value is None else json.dumps(value))
+
+    return get
+
+
+def test_a_fetched_receipt_must_be_its_named_attempts_receipt(tmp_path):
+    _, _, receipt_a, receipt_b = _ledger_with_attempts(tmp_path)
+    pointer = {"attempt_key": "b", "sequence": 2}
+    good = _remote(
+        {
+            f"{PAIR.pair_id}/qualification_receipt.json": receipt_b.canonical_payload(),
+            "/latest_attempt.json": pointer,
+            "/attempts/b/qualification_receipt.json": receipt_b.canonical_payload(),
+        }
+    )
+    assert (
+        fetch_ledger_receipt("uk", PAIR.pair_id, get=good).unreviewed_sha256
+        == receipt_b.unreviewed_sha256
+    )
+    merged = _remote(
+        {
+            f"{PAIR.pair_id}/qualification_receipt.json": _hybrid(receipt_a, receipt_b),
+            "/latest_attempt.json": pointer,
+            "/attempts/b/qualification_receipt.json": receipt_b.canonical_payload(),
+        }
+    )
+    with pytest.raises(LedgerInconsistency):
+        fetch_ledger_receipt("uk", PAIR.pair_id, get=merged)
+    unnamed = _remote(
+        {f"{PAIR.pair_id}/qualification_receipt.json": receipt_b.canonical_payload()}
+    )
+    with pytest.raises(LedgerInconsistency, match="names no attempt"):
+        fetch_ledger_receipt("uk", PAIR.pair_id, get=unnamed)
+    assert fetch_ledger_receipt("uk", PAIR.pair_id, get=_remote({})) is None
diff --git a/tests/test_qualification_ledger_script.py b/tests/test_qualification_ledger_script.py
new file mode 100644
index 00000000..5e71953f
--- /dev/null
+++ b/tests/test_qualification_ledger_script.py
@@ -0,0 +1,111 @@
+"""The ledger publish script (.github/qualification-ledger.sh) under concurrency.
+
+Invariant: publishes from concurrent runs never lose evidence. Each run's new
+directory ends up on the ledger branch, including when both runs found no
+ledger yet and each made the branch's first commit.
+"""
+
+from __future__ import annotations
+
+import os
+import shutil
+import subprocess
+from pathlib import Path
+
+import pytest
+
+SCRIPT = Path(__file__).resolve().parents[1] / ".github" / "qualification-ledger.sh"
+
+pytestmark = pytest.mark.skipif(shutil.which("git") is None, reason="needs git")
+
+
+def _run(*args, cwd, check=True):
+    env = {
+        **os.environ,
+        "GIT_AUTHOR_NAME": "test",
+        "GIT_AUTHOR_EMAIL": "test@example.org",
+        "GIT_COMMITTER_NAME": "test",
+        "GIT_COMMITTER_EMAIL": "test@example.org",
+        "GIT_TERMINAL_PROMPT": "0",
+        "LEDGER_RETRY_DELAY": "0",
+    }
+    return subprocess.run(
+        args,
+        cwd=cwd,
+        env=env,
+        check=check,
+        text=True,
+        stdout=subprocess.PIPE,
+        stderr=subprocess.STDOUT,
+    )
+
+
+def _clone(root: Path, origin: Path, name: str) -> Path:
+    clone = root / name
+    _run(
+        "git", "clone", "--quiet", "--branch", "main", str(origin), str(clone), cwd=root
+    )
+    return clone
+
+
+def _origin(root: Path) -> Path:
+    origin = root / "origin.git"
+    _run("git", "init", "--quiet", "--bare", "-b", "main", str(origin), cwd=root)
+    seed = root / "seed"
+    _run("git", "clone", "--quiet", str(origin), str(seed), cwd=root)
+    (seed / "seed").write_text("seed")
+    _run("git", "add", "seed", cwd=seed)
+    _run("git", "commit", "--quiet", "-m", "seed", cwd=seed)
+    _run("git", "push", "--quiet", "origin", "HEAD:refs/heads/main", cwd=seed)
+    return origin
+
+
+def _ledger_files(origin: Path) -> set[str]:
+    listing = _run(
+        "git", "ls-tree", "-r", "--name-only", "uk-qualification-ledger", cwd=origin
+    )
+    return set(listing.stdout.split())
+
+
+@pytest.mark.parametrize("ledger_exists", [False, True])
+def test_concurrent_publishes_keep_both_runs_evidence(tmp_path, ledger_exists):
+    """Interrupted review of #574: the second first-ever publish failed on HEAD~1."""
+
+    origin = _origin(tmp_path)
+    if ledger_exists:
+        first = _clone(tmp_path, origin, "first")
+        _run("bash", str(SCRIPT), "checkout", "ledger", cwd=first)
+        (first / "ledger" / "existing").write_text("existing")
+        _run("bash", str(SCRIPT), "publish", "ledger", "existing", cwd=first)
+    clones = []
+    for name in ("a", "b"):
+        clone = _clone(tmp_path, origin, name)
+        _run("bash", str(SCRIPT), "checkout", "ledger", cwd=clone)
+        (clone / "ledger" / "uk" / name).mkdir(parents=True)
+        (clone / "ledger" / "uk" / name / "receipt.json").write_text(name)
+        clones.append(clone)
+    for clone, name in zip(clones, ("a", "b")):
+        result = _run(
+            "bash",
+            str(SCRIPT),
+            "publish",
+            "ledger",
+            f"attempt {name}",
+            cwd=clone,
+            check=False,
+        )
+        assert result.returncode == 0, result.stdout
+    files = _ledger_files(origin)
+    assert {"uk/a/receipt.json", "uk/b/receipt.json"} <= files
+    if ledger_exists:
+        assert "existing" in files
+
+
+def test_an_unchanged_ledger_publishes_nothing(tmp_path):
+    origin = _origin(tmp_path)
+    clone = _clone(tmp_path, origin, "a")
+    _run("bash", str(SCRIPT), "checkout", "ledger", cwd=clone)
+    (clone / "ledger" / "x").write_text("x")
+    _run("bash", str(SCRIPT), "publish", "ledger", "first", cwd=clone)
+    again = _run("bash", str(SCRIPT), "publish", "ledger", "again", cwd=clone)
+    assert "Ledger unchanged." in again.stdout
diff --git a/tests/test_qualified_pair_certification.py b/tests/test_qualified_pair_certification.py
new file mode 100644
index 00000000..953dd192
--- /dev/null
+++ b/tests/test_qualified_pair_certification.py
@@ -0,0 +1,594 @@
+"""Certification on an automated qualification of the exact pair.
+
+Invariants:
+
+- ``qualified_exact_pair`` is used only when neither the build-time model
+  nor a publisher claim covers the model; it is exact (one model version,
+  one core version, one data build) and names the qualified pair;
+- the core must be the build core, publisher-claimed, or qualified;
+- at runtime a qualified certificate is honoured only for exactly its model
+  version and data build, online and offline;
+- a bundle certificate on that basis needs a promotable ledger receipt for
+  exactly what it certifies (``verify_certification_evidence``).
+"""
+
+from __future__ import annotations
+
+import copy
+import json
+from pathlib import Path
+from unittest.mock import MagicMock, patch
+
+import pytest
+from hypothesis import given, settings
+from hypothesis import strategies as st
+
+from policyengine.provenance.certification import (
+    BASIS_QUALIFIED_PAIR,
+    CertificationError,
+    build_country_manifest_payload,
+    validate_release_manifest,
+)
+from policyengine.provenance.manifest import (
+    DataCertification,
+    DataReleaseManifest,
+    DataReleaseManifestUnavailableError,
+    certify_data_release_compatibility,
+    get_data_release_manifest,
+)
+from policyengine.provenance.qualification_ledger import (
+    latest_receipts,
+    pending_attempts,
+    verify_certification_evidence,
+    write_json,
+)
+from tests.qualification_fixtures import gates, make_pair, make_receipt
+
+PAIR_ID = "a" * 64
+
+
+def _uk_manifest(
+    model_claims=("==2.122.2",), core_claims=("==3.32.13",)
+) -> DataReleaseManifest:
+    return DataReleaseManifest.model_validate(
+        {
+            "schema_version": 1,
+            "data_package": {"name": "policyengine-uk-data", "version": "1.58.0"},
+            "compatible_model_packages": [
+                {"name": "policyengine-uk", "specifier": spec} for spec in model_claims
+            ],
+            "compatible_core_packages": [
+                {"name": "policyengine-core", "specifier": spec} for spec in core_claims
+            ],
+            "default_datasets": {"national": "enhanced_frs_2024_25"},
+            "build": {
+                "build_id": "policyengine-uk-data-1.58.0",
+                "built_with_core_package": {
+                    "name": "policyengine-core",
+                    "version": "3.32.13",
+                },
+                "built_with_model_package": {
+                    "name": "policyengine-uk",
+                    "version": "2.122.2",
+                    "data_build_fingerprint": "sha256:build",
+                },
+            },
+            "artifacts": {
+                "enhanced_frs_2024_25": {
+                    "kind": "microdata",
+                    "path": "enhanced_frs_2024_25.h5",
+                    "repo_id": "policyengine/policyengine-uk-data-private",
+                    "revision": "1.58.0",
+                    "sha256": "d" * 64,
+                    "size_bytes": 10,
+                }
+            },
+        }
+    )
+
+
+class TestValidateWithQualification:
+    def test_uncovered_model_is_admitted_only_with_a_pair_id(self):
+        with pytest.raises(CertificationError, match="qualification"):
+            validate_release_manifest(_uk_manifest(), "policyengine-uk", "2.126.8")
+        basis, warnings = validate_release_manifest(
+            _uk_manifest(core_claims=("==3.33.0",)),
+            "policyengine-uk",
+            "2.126.8",
+            core_version="3.33.0",
+            qualified_pair_id=PAIR_ID,
+        )
+        assert basis == BASIS_QUALIFIED_PAIR
+        assert any("exactly policyengine-uk==2.126.8" in item for item in warnings)
+
+    def test_stronger_bases_win_over_qualification(self):
+        basis, _ = validate_release_manifest(
+            _uk_manifest(), "policyengine-uk", "2.122.2", qualified_pair_id=PAIR_ID
+        )
+        assert basis == "built_with_model_package"
+        basis, _ = validate_release_manifest(
+            _uk_manifest(model_claims=("==2.122.2", "==2.126.8")),
+            "policyengine-uk",
+            "2.126.8",
+            qualified_pair_id=PAIR_ID,
+        )
+        assert basis == "legacy_compatible_model_package"
+
+    @pytest.mark.parametrize(
+        ("model_version", "model_claims"),
+        [("2.122.2", ("==2.122.2",)), ("2.126.8", ("==2.122.2", "==2.126.8"))],
+        ids=["build-time model", "publisher-claimed model"],
+    )
+    def test_an_unclaimed_core_makes_the_basis_the_exact_pair(
+        self, model_version, model_claims
+    ):
+        """Interrupted review of #574: with the model on a stronger basis, a
+        core the publisher never claimed was certified on that basis, and the
+        certificate named no pair, so no evidence was ever required."""
+
+        basis, warnings = validate_release_manifest(
+            _uk_manifest(model_claims=model_claims),
+            "policyengine-uk",
+            model_version,
+            core_version="9.99.0",
+            qualified_pair_id=PAIR_ID,
+        )
+        assert basis == BASIS_QUALIFIED_PAIR
+        assert any("policyengine-core==9.99.0" in item for item in warnings)
+        with pytest.raises(CertificationError, match="policyengine-core 9.99.0"):
+            validate_release_manifest(
+                _uk_manifest(model_claims=model_claims),
+                "policyengine-uk",
+                model_version,
+                core_version="9.99.0",
+            )
+
+    def test_qualified_pair_needs_its_core_version(self):
+        with pytest.raises(CertificationError, match="core"):
+            validate_release_manifest(
+                _uk_manifest(), "policyengine-uk", "2.126.8", qualified_pair_id=PAIR_ID
+            )
+
+    def test_core_outside_claims_is_refused_unless_qualified(self):
+        with pytest.raises(CertificationError, match="policyengine-core 3.34.0"):
+            validate_release_manifest(
+                _uk_manifest(model_claims=("==2.126.8",)),
+                "policyengine-uk",
+                "2.126.8",
+                core_version="3.34.0",
+            )
+        basis, warnings = validate_release_manifest(
+            _uk_manifest(),
+            "policyengine-uk",
+            "2.126.8",
+            core_version="3.34.0",
+            qualified_pair_id=PAIR_ID,
+        )
+        assert basis == BASIS_QUALIFIED_PAIR
+        assert any("outside the publisher's core claims" in item for item in warnings)
+
+    def test_build_core_and_claimed_core_pass(self):
+        for core in ("3.32.13",):
+            validate_release_manifest(
+                _uk_manifest(), "policyengine-uk", "2.122.2", core_version=core
+            )
+
+    def test_payload_names_the_pair_and_core(self):
+        manifest = _uk_manifest()
+        payload = build_country_manifest_payload(
+            country="uk",
+            manifest=manifest,
+            uri_parts={
+                "repo_type": "model",
+                "repo_id": "policyengine/policyengine-uk-data-private",
+                "revision": "f" * 40,
+                "path": "releases/1.58.0/release_manifest.json",
+            },
+            policyengine_version="6.2.7",
+            model_package="policyengine-uk",
+            model_version="2.126.8",
+            model_wheel={
+                "sha256": "a" * 64,
+                "url": "https://files.pythonhosted.org/x.whl",
+            },
+            compatibility_basis=BASIS_QUALIFIED_PAIR,
+            core_version="3.33.0",
+            qualified_pair_id=PAIR_ID,
+        )
+        certification = payload["certification"]
+        assert certification["compatibility_basis"] == BASIS_QUALIFIED_PAIR
+        assert certification["certified_for_core_version"] == "3.33.0"
+        assert certification["qualification_pair_id"] == PAIR_ID
+        assert certification["qualification_ledger_url"].endswith(f"/uk/{PAIR_ID}")
+
+
+def _response(payload):
+    response = MagicMock()
+    response.status_code = 200
+    response.text = json.dumps(payload)
+    response.content = response.text.encode()
+    response.raise_for_status.return_value = None
+    return response
+
+
+def _qualified(model="2.126.8", build_id="policyengine-uk-data-1.58.0"):
+    return DataCertification(
+        compatibility_basis="qualified_exact_pair",
+        certified_for_model_version=model,
+        certified_for_core_version="3.33.0",
+        data_build_id=build_id,
+        data_build_fingerprint="sha256:build",
+        qualification_pair_id=PAIR_ID,
+    )
+
+
+class TestRuntimeAcceptance:
+    def setup_method(self):
+        get_data_release_manifest.cache_clear()
+
+    def _certify(
+        self, bundled, runtime_model, remote=None, fingerprint=None, core="3.33.0"
+    ):
+        manifest = MagicMock(certification=bundled)
+        manifest.model_package.name = "policyengine-uk"
+        manifest.data_package = MagicMock(
+            release_manifest_revision=None, repo_id="r", version="1.58.0"
+        )
+        patches = [
+            patch(
+                "policyengine.provenance.manifest.get_release_manifest",
+                return_value=manifest,
+            )
+        ]
+        if remote is None:
+            patches.append(
+                patch(
+                    "policyengine.provenance.manifest.get_data_release_manifest",
+                    side_effect=DataReleaseManifestUnavailableError("private"),
+                )
+            )
+        else:
+            patches.append(
+                patch(
+                    "policyengine.provenance.manifest.get_data_release_manifest",
+                    return_value=remote,
+                )
+            )
+        with patches[0], patches[1]:
+            return certify_data_release_compatibility(
+                "uk",
+                runtime_model_version=runtime_model,
+                runtime_data_build_fingerprint=fingerprint,
+                runtime_core_version=core,
+            )
+
+    def test_online_exact_model_and_build_is_honoured(self):
+        certification = self._certify(_qualified(), "2.126.8", remote=_uk_manifest())
+        assert certification.compatibility_basis == "qualified_exact_pair"
+
+    def test_online_other_model_version_is_refused(self):
+        with pytest.raises(ValueError, match="not certified"):
+            self._certify(_qualified(), "2.126.9", remote=_uk_manifest())
+
+    def test_online_other_data_build_is_refused(self):
+        with pytest.raises(ValueError, match="not certified"):
+            self._certify(
+                _qualified(build_id="policyengine-uk-data-1.59.0"),
+                "2.126.8",
+                remote=_uk_manifest(),
+            )
+
+    def test_online_other_core_is_refused(self):
+        """Interrupted review of #574: an exact pair was honoured whatever core ran."""
+
+        with pytest.raises(ValueError, match="not certified"):
+            self._certify(_qualified(), "2.126.8", remote=_uk_manifest(), core="3.34.0")
+
+    @pytest.mark.parametrize(
+        ("runtime_model", "fingerprint", "claims"),
+        [
+            ("2.122.2", None, ()),
+            ("2.126.9", "sha256:build", ()),
+            ("2.126.9", None, ("==2.126.9",)),
+        ],
+        ids=["build-model", "fingerprint", "publisher-claim"],
+    )
+    def test_online_stronger_basis_cannot_bypass_qualified_model(
+        self, runtime_model, fingerprint, claims
+    ):
+        """The qualified core must not extend qualification to another model."""
+
+        with pytest.raises(ValueError, match="not certified.*model"):
+            self._certify(
+                _qualified(),
+                runtime_model,
+                remote=_uk_manifest(model_claims=claims),
+                fingerprint=fingerprint,
+            )
+
+    @pytest.mark.parametrize(
+        ("runtime_model", "fingerprint", "claims"),
+        [
+            ("2.122.2", None, ()),
+            ("2.126.8", "sha256:build", ()),
+            ("2.126.8", None, ("==2.126.8",)),
+        ],
+        ids=["build-model", "fingerprint", "publisher-claim"],
+    )
+    @pytest.mark.parametrize("remote_build", ["another-build", None])
+    def test_online_stronger_basis_cannot_bypass_qualified_build(
+        self, runtime_model, fingerprint, claims, remote_build
+    ):
+        remote = _uk_manifest(model_claims=claims)
+        remote.build.build_id = remote_build
+        with pytest.raises(ValueError, match="not certified.*data build"):
+            self._certify(
+                _qualified(model=runtime_model),
+                runtime_model,
+                remote=remote,
+                fingerprint=fingerprint,
+            )
+
+    def test_online_publisher_claim_cannot_bypass_missing_qualified_build(self):
+        remote = _uk_manifest(model_claims=("==2.126.8",))
+        remote.build = None
+        with pytest.raises(ValueError, match="not certified.*data build"):
+            self._certify(_qualified(), "2.126.8", remote=remote)
+
+    @pytest.mark.parametrize(
+        ("runtime_model", "fingerprint", "claims", "expected_basis"),
+        [
+            ("2.122.2", None, (), "exact_build_model_version"),
+            (
+                "2.126.8",
+                "sha256:build",
+                (),
+                "matching_data_build_fingerprint",
+            ),
+            (
+                "2.126.8",
+                None,
+                ("==2.126.8",),
+                "legacy_compatible_model_package",
+            ),
+        ],
+        ids=["build-model", "fingerprint", "publisher-claim"],
+    )
+    def test_online_stronger_basis_cannot_bypass_qualified_core(
+        self, runtime_model, fingerprint, claims, expected_basis
+    ):
+        remote = _uk_manifest(model_claims=claims)
+        bundled = _qualified(model=runtime_model)
+        assert (
+            self._certify(
+                bundled, runtime_model, remote=remote, fingerprint=fingerprint
+            ).compatibility_basis
+            == expected_basis
+        )
+        with pytest.raises(ValueError, match="not certified"):
+            self._certify(
+                bundled,
+                runtime_model,
+                remote=remote,
+                fingerprint=fingerprint,
+                core="99.0.0",
+            )
+
+    def test_online_build_model_requires_installed_qualified_core(self):
+        from importlib.metadata import PackageNotFoundError
+
+        with patch(
+            "importlib.metadata.version",
+            side_effect=PackageNotFoundError("policyengine-core"),
+        ):
+            with pytest.raises(ValueError, match="not certified"):
+                self._certify(
+                    _qualified(model="2.122.2"),
+                    "2.122.2",
+                    remote=_uk_manifest(),
+                    core=None,
+                )
+
+    def test_offline_other_core_is_not_certified(self):
+        certification = self._certify(_qualified(), "2.126.8", core="3.34.0")
+        assert certification.compatibility_basis == (
+            "unverified_data_release_manifest_unavailable"
+        )
+
+    def test_the_installed_core_is_read_when_none_is_given(self):
+        with patch("importlib.metadata.version", return_value="3.34.0") as installed:
+            with pytest.raises(ValueError, match="not certified"):
+                self._certify(_qualified(), "2.126.8", remote=_uk_manifest(), core=None)
+        installed.assert_called_with("policyengine-core")
+
+    def test_other_bases_make_no_runtime_core_claim(self):
+        legacy = DataCertification(
+            compatibility_basis="legacy_compatible_model_package",
+            certified_for_model_version="2.126.8",
+            certified_for_core_version="3.33.0",
+            data_build_id="policyengine-uk-data-1.58.0",
+        )
+        assert (
+            self._certify(legacy, "2.126.8", core="3.34.0").compatibility_basis
+            == "legacy_compatible_model_package"
+        )
+
+    def test_offline_fingerprint_difference_is_expected(self):
+        certification = self._certify(
+            _qualified(), "2.126.8", fingerprint="sha256:runtime"
+        )
+        assert certification.compatibility_basis == "qualified_exact_pair"
+
+
+# --- Ledger evidence ---------------------------------------------------------
+
+
+def _record_for(receipt):
+    pair = receipt.pair
+    return {
+        "certification": {
+            "compatibility_basis": "qualified_exact_pair",
+            "qualification_pair_id": pair.pair_id,
+            "certified_for_model_version": pair.model.version,
+            "certified_for_core_version": pair.core.version,
+            "data_build_id": pair.data.build_id,
+        },
+        "model_package": {
+            "version": pair.model.version,
+            "sha256": pair.model.wheel_sha256,
+        },
+        "data_package": {
+            "version": pair.data.release_version,
+            "release_manifest_revision": pair.data.release_manifest_revision,
+        },
+        "default_dataset": pair.data.dataset,
+        "certified_data_artifact": {
+            "dataset": pair.data.dataset,
+            "sha256": pair.data.dataset_sha256,
+        },
+        "datasets": {
+            pair.data.dataset: {
+                "repo_id": pair.data.repo_id,
+                "path": pair.data.dataset_path,
+                "revision": pair.data.dataset_revision,
+                "sha256": pair.data.dataset_sha256,
+            }
+        },
+    }
+
+
+def test_other_bases_need_no_ledger_evidence():
+    assert (
+        verify_certification_evidence(
+            {
+                "certification": {
+                    "compatibility_basis": "legacy_compatible_model_package"
+                }
+            },
+            None,
+        )
+        == []
+    )
+
+
+def test_matching_promotable_receipt_is_sufficient():
+    receipt = make_receipt()
+    assert verify_certification_evidence(_record_for(receipt), receipt) == []
+
+
+REFERENCE_FIELDS = ["repo_id", "path", "revision", "sha256"]
+
+
+@pytest.mark.parametrize("field", REFERENCE_FIELDS)
+def test_the_dataset_reference_the_runtime_resolves_is_evidence_too(field):
+    """Interrupted review of #574: a record pointing its default dataset at
+    another repository, path, revision or file verified against the receipt."""
+
+    receipt = make_receipt()
+    record = _record_for(receipt)
+    record["datasets"][receipt.pair.data.dataset][field] = "f" * 64
+    problems = verify_certification_evidence(record, receipt)
+    assert any("dataset reference" in problem for problem in problems)
+    del record["datasets"][receipt.pair.data.dataset]
+    assert verify_certification_evidence(record, receipt)
+
+
+def test_a_record_without_the_model_wheel_hash_is_refused():
+    receipt = make_receipt()
+    record = _record_for(receipt)
+    del record["model_package"]["sha256"]
+    problems = verify_certification_evidence(record, receipt)
+    assert any("model wheel sha256" in problem for problem in problems)
+
+
+def test_missing_or_unpromotable_receipt_is_refused():
+    receipt = make_receipt()
+    record = _record_for(receipt)
+    assert verify_certification_evidence(record, None)
+    pending = make_receipt(gate_results=gates(replay="needs_review"))
+    assert verify_certification_evidence(_record_for(pending), pending)
+
+
+FIELDS = [
+    ("certification", "certified_for_model_version"),
+    ("certification", "certified_for_core_version"),
+    ("certification", "data_build_id"),
+    ("model_package", "version"),
+    ("model_package", "sha256"),
+    ("data_package", "version"),
+    ("data_package", "release_manifest_revision"),
+    ("certified_data_artifact", "sha256"),
+    ("certified_data_artifact", "dataset"),
+]
+
+
+@settings(max_examples=60, deadline=None)
+@given(field=st.sampled_from(FIELDS), value=st.text(min_size=1, max_size=8))
+def test_any_certified_field_differing_from_the_receipt_is_refused(field, value):
+    receipt = make_receipt()
+    record = copy.deepcopy(_record_for(receipt))
+    if record[field[0]][field[1]] == value:
+        return
+    record[field[0]][field[1]] = value
+    assert verify_certification_evidence(record, receipt)
+
+
+def test_ledger_listing(tmp_path):
+    qualified = make_receipt()
+    pair_dir = tmp_path / "uk" / qualified.pair_id
+    # A published receipt counts only as its latest attempt's own receipt.
+    write_json(
+        pair_dir / "attempts" / "a" / "qualification_receipt.json",
+        qualified.canonical_payload(),
+    )
+    write_json(pair_dir / "qualification_receipt.json", qualified.canonical_payload())
+    pending = (
+        tmp_path / "uk" / ("b" * 64) / "attempts" / "abc" / "replay" / "request.json"
+    )
+    write_json(pending, {"schema": "x"})
+    assert [r.pair_id for r in latest_receipts(tmp_path)] == [qualified.pair_id]
+    assert pending_attempts(tmp_path) == [pending.parent.parent]
+
+
+def test_committed_bundle_has_ledger_evidence_for_qualified_pairs():
+    """The release gate: an exact-pair certificate ships only with its evidence."""
+
+    from policyengine.provenance.qualification_ledger import fetch_ledger_receipt
+
+    bundle_path = (
+        Path(__file__).resolve().parents[1]
+        / "src"
+        / "policyengine"
+        / "data"
+        / "bundle"
+        / "manifest.json"
+    )
+    bundle = json.loads(bundle_path.read_text())
+    checked = 0
+    for country_id, record in bundle["data_releases"].items():
+        certification = record.get("certification") or {}
+        if certification.get("compatibility_basis") != "qualified_exact_pair":
+            continue
+        receipt = fetch_ledger_receipt(
+            country_id, certification["qualification_pair_id"]
+        )
+        assert verify_certification_evidence(record, receipt) == []
+        checked += 1
+    qualified = [
+        country_id
+        for country_id, record in bundle["data_releases"].items()
+        if (record.get("certification") or {}).get("compatibility_basis")
+        == "qualified_exact_pair"
+    ]
+    assert checked == len(qualified)
+
+
+def test_pair_id_changes_with_any_pinned_identity():
+    base = make_pair()
+    for variant in (
+        make_pair(model="2.126.9"),
+        make_pair(core="3.33.1"),
+        make_pair(data="1.59.0"),
+        make_pair(dataset_sha="0" * 64),
+    ):
+        assert variant.pair_id != base.pair_id
diff --git a/tests/test_uk_candidate_gates.py b/tests/test_uk_candidate_gates.py
new file mode 100644
index 00000000..1f680dc2
--- /dev/null
+++ b/tests/test_uk_candidate_gates.py
@@ -0,0 +1,3750 @@
+"""Mandatory local qualification gates (no network, no credentials).
+
+Invariants:
+
+- installed files match the wheel byte for byte, or the gate fails: mutating
+  any single installed file fails it;
+- no gate can skip: a missing harness is ``unavailable`` and a crash is
+  ``error``; ``run_gates`` returns exactly one result per gate;
+- inputs newly missing from the data are exactly (missing now) − (missing
+  before); a critical input that becomes unusable always fails;
+- metric review is monotone: shrinking every move toward zero never makes
+  the status worse.
+"""
+
+from __future__ import annotations
+
+import base64
+import copy
+import hashlib
+import io
+import json
+import sys
+import zipfile
+from pathlib import Path
+from types import SimpleNamespace
+
+import pandas as pd
+import pytest
+from hypothesis import given, settings
+from hypothesis import strategies as st
+
+from policyengine.provenance import uk_candidate as gates_module
+from policyengine.provenance.manifest import DataReleaseManifest
+from policyengine.provenance.uk_candidate import (
+    budget_inputs,
+    compare_metric_reports,
+    core_compatibility,
+    data_access,
+    dataset_bytes,
+    installed_versions,
+    metrics_review,
+    reform_contract,
+    run_gates,
+    supported_years,
+    wheel_hashes,
+)
+from policyengine.provenance.wheels import (
+    parse_record,
+    record_digest_to_hex,
+    verify_installed_against_wheel,
+)
+from tests.qualification_fixtures import HARNESS, make_pair, make_receipt
+
+# --- Installed versions --------------------------------------------------------
+
+
+class FakeMetadata:
+    PackageNotFoundError = KeyError
+
+    def __init__(self, versions, requires):
+        self.versions, self._requires = versions, requires
+
+    def version(self, name):
+        return self.versions[name]
+
+    def requires(self, name):
+        return self._requires.get(name, [])
+
+
+def test_installed_versions_pass_and_fail():
+    pair = make_pair()
+    versions = {
+        "policyengine": "6.2.6",
+        "policyengine-uk": "2.126.8",
+        "policyengine-core": "3.33.0",
+        "numpy": "2.1.0",
+    }
+    requires = {
+        "policyengine-uk": ["policyengine-core>=3.32.27", "numpy>=1.26"],
+        "policyengine": ["pydantic; extra == 'docs'"],
+    }
+    result, _ = installed_versions(
+        pair, HARNESS, metadata=FakeMetadata(versions, requires)
+    )
+    assert result.status == "pass", result.summary
+    result, evidence = installed_versions(
+        pair,
+        HARNESS,
+        metadata=FakeMetadata({**versions, "policyengine-uk": "2.126.6"}, requires),
+    )
+    assert result.status == "fail" and "2.126.6" in result.summary
+    result, _ = installed_versions(
+        pair,
+        HARNESS,
+        metadata=FakeMetadata({**versions, "policyengine-core": "3.30.0"}, requires),
+    )
+    assert result.status == "fail"
+    result, _ = installed_versions(
+        pair,
+        HARNESS,
+        metadata=FakeMetadata(
+            versions, {**requires, "policyengine-core": ["missing-pkg>=1"]}
+        ),
+    )
+    assert result.status == "fail" and "missing-pkg" in result.summary
+
+
+# --- Wheel RECORD verification ----------------------------------------------------
+
+
+def _b64(data: bytes) -> str:
+    return base64.urlsafe_b64encode(hashlib.sha256(data).digest()).decode().rstrip("=")
+
+
+FILES = {
+    "policyengine_uk/__init__.py": b"VERSION = '2.126.8'\n",
+    "policyengine_uk/system.py": b"system = object()\n",
+    "policyengine_uk/parameters/rate.yaml": b"values:\n  2027-04-06: 0.2\n",
+}
+
+
+def _build(tmp_path: Path, files=FILES):
+    dist_info = "policyengine_uk-2.126.8.dist-info"
+    record_lines = [
+        f"{path},sha256={_b64(data)},{len(data)}" for path, data in files.items()
+    ]
+    record_lines.append(f"{dist_info}/RECORD,,")
+    wheel = tmp_path / "policyengine_uk-2.126.8-py3-none-any.whl"
+    with zipfile.ZipFile(wheel, "w") as archive:
+        for path, data in files.items():
+            archive.writestr(path, data)
+        archive.writestr(f"{dist_info}/RECORD", "\n".join(record_lines) + "\n")
+    site = tmp_path / "site"
+    for path, data in files.items():
+        (site / path).parent.mkdir(parents=True, exist_ok=True)
+        (site / path).write_bytes(data)
+    installed_record = record_lines + [
+        f"{dist_info}/INSTALLER,sha256={_b64(b'uv')},2",
+        f"{dist_info}/REQUESTED,,",
+    ]
+    return wheel, site, installed_record
+
+
+class FakeDistribution:
+    def __init__(self, site: Path, record_lines):
+        self.site = site
+        self.files = []
+        for line in record_lines:
+            path, digest, *_ = line.split(",")
+            hash_ = (
+                SimpleNamespace(mode="sha256", value=digest.split("=", 1)[1])
+                if digest
+                else None
+            )
+            self.files.append(SimpleNamespace(hash=hash_, __str__=None, path=path))
+
+    def locate_file(self, path):
+        return self.site / path
+
+
+class _File(str):
+    pass
+
+
+def _distribution(site, record_lines):
+    dist = SimpleNamespace(site=site, files=[], locate_file=lambda path: site / path)
+    for line in record_lines:
+        path, digest, *_ = line.split(",")
+        item = _File(path)
+        item.hash = (
+            SimpleNamespace(mode="sha256", value=digest.split("=", 1)[1])
+            if digest
+            else None
+        )
+        dist.files.append(item)
+    return dist
+
+
+def _metadata_for(site, record_lines):
+    return SimpleNamespace(distribution=lambda name: _distribution(site, record_lines))
+
+
+def test_record_hash_decoding():
+    data = b"hello"
+    assert (
+        record_digest_to_hex(f"sha256={_b64(data)}") == hashlib.sha256(data).hexdigest()
+    )
+    parsed = parse_record(f"a.py,sha256={_b64(data)},5\nRECORD,,\n")
+    assert parsed == {"a.py": hashlib.sha256(data).hexdigest(), "RECORD": None}
+    with pytest.raises(ValueError):
+        record_digest_to_hex("md5=abc")
+
+
+def test_untouched_install_verifies(tmp_path):
+    wheel, site, record = _build(tmp_path)
+    evidence = verify_installed_against_wheel(
+        "policyengine-uk", wheel, metadata=_metadata_for(site, record)
+    )
+    assert evidence["problems"] == []
+    assert evidence["files_verified"] == len(FILES)
+
+
+@settings(max_examples=30, deadline=None)
+@given(index=st.integers(0, len(FILES) - 1), extra=st.binary(min_size=1, max_size=8))
+def test_any_mutated_installed_file_fails(tmp_path_factory, index, extra):
+    tmp_path = tmp_path_factory.mktemp("wheel")
+    wheel, site, record = _build(tmp_path)
+    target = site / list(FILES)[index]
+    target.write_bytes(target.read_bytes() + extra)
+    evidence = verify_installed_against_wheel(
+        "policyengine-uk", wheel, metadata=_metadata_for(site, record)
+    )
+    assert evidence["problems"]
+
+
+def test_extra_installed_file_fails(tmp_path):
+    wheel, site, record = _build(tmp_path)
+    (site / "policyengine_uk" / "injected.py").write_bytes(b"x = 1\n")
+    record = record + [f"policyengine_uk/injected.py,sha256={_b64(b'x = 1')},6"]
+    evidence = verify_installed_against_wheel(
+        "policyengine-uk", wheel, metadata=_metadata_for(site, record)
+    )
+    assert any("injected.py" in problem for problem in evidence["problems"])
+
+
+def test_wheel_hash_gate(tmp_path):
+    wheel, site, record = _build(tmp_path)
+    digest = hashlib.sha256(wheel.read_bytes()).hexdigest()
+    pair = make_pair()
+    pair = pair.model_copy(
+        update={"model": pair.model.model_copy(update={"wheel_sha256": digest})}
+    )
+    (tmp_path / "core").mkdir()
+    core_wheel, core_site, core_record = _build(tmp_path / "core")
+    core_digest = hashlib.sha256(core_wheel.read_bytes()).hexdigest()
+    pair = pair.model_copy(
+        update={"core": pair.core.model_copy(update={"wheel_sha256": core_digest})}
+    )
+    metadata = SimpleNamespace(
+        distribution=lambda name: (
+            _distribution(site, record)
+            if name == "policyengine-uk"
+            else _distribution(core_site, core_record)
+        )
+    )
+    result, _ = wheel_hashes(
+        pair,
+        wheel_paths={"policyengine-uk": wheel, "policyengine-core": core_wheel},
+        pypi_digests={"policyengine-uk": digest, "policyengine-core": core_digest},
+        metadata=metadata,
+    )
+    assert result.status == "pass", result.summary
+    result, _ = wheel_hashes(
+        pair,
+        wheel_paths={"policyengine-uk": wheel, "policyengine-core": core_wheel},
+        pypi_digests={"policyengine-uk": "0" * 64, "policyengine-core": core_digest},
+        metadata=metadata,
+    )
+    assert result.status == "fail"
+
+
+# --- Core compatibility, data access, bytes, years -----------------------------------
+
+
+def _manifest(model_claims=(), core_claims=()):
+    return DataReleaseManifest.model_validate(
+        {
+            "schema_version": 1,
+            "data_package": {"name": "policyengine-uk-data", "version": "1.58.0"},
+            "compatible_model_packages": [
+                {"name": "policyengine-uk", "specifier": s} for s in model_claims
+            ],
+            "compatible_core_packages": [
+                {"name": "policyengine-core", "specifier": s} for s in core_claims
+            ],
+            "default_datasets": {"national": "enhanced_frs_2024_25"},
+            "build": {
+                "build_id": "b",
+                "built_with_model_package": {
+                    "name": "policyengine-uk",
+                    "version": "2.122.2",
+                },
+            },
+            "artifacts": {
+                "enhanced_frs_2024_25": {
+                    "kind": "microdata",
+                    "path": "enhanced_frs_2024_25.h5",
+                    "repo_id": "policyengine/policyengine-uk-data-private",
+                    "revision": "1.58.0",
+                    "sha256": "d" * 64,
+                },
+                "frs_2024_25": {
+                    "kind": "microdata",
+                    "path": "frs_2024_25.h5",
+                    "repo_id": "policyengine/policyengine-uk-data-private",
+                    "revision": "1.58.0",
+                },
+            },
+        }
+    )
+
+
+def test_core_compatibility():
+    pair = make_pair()
+    result, evidence = core_compatibility(
+        pair, _manifest(), model_requires_core=">=3.32.27"
+    )
+    assert result.status == "pass" and evidence["publisher_claim_covers_model"] is False
+    result, _ = core_compatibility(
+        pair, _manifest(["==2.126.8"], ["==3.33.0"]), model_requires_core=">=3.32.27"
+    )
+    assert result.details == {
+        "publisher_claim_covers_model": True,
+        "publisher_claim_covers_core": True,
+    }
+    result, _ = core_compatibility(pair, _manifest(), model_requires_core=">=3.34")
+    assert result.status == "fail"
+
+
+class _Response:
+    def __init__(self, status, content=b""):
+        self.status_code, self.content = status, content
+
+
+def test_data_access():
+    pair = make_pair()
+    manifest_bytes = _manifest().model_dump_json().encode()
+    pair = pair.model_copy(
+        update={
+            "data": pair.data.model_copy(
+                update={
+                    "release_manifest_sha256": hashlib.sha256(
+                        manifest_bytes
+                    ).hexdigest()
+                }
+            )
+        }
+    )
+
+    def get(url, headers=None, timeout=None):
+        return _Response(200 if headers else 401, manifest_bytes)
+
+    def head(url, headers=None, timeout=None, allow_redirects=True):
+        return _Response(200 if headers else 401)
+
+    result, evidence, manifest = data_access(pair, get=get, head=head, token="t")
+    assert result.status == "pass" and manifest is not None
+    assert evidence["anonymous_dataset_status"] == 401
+    result, _, _ = data_access(pair, get=get, head=head, token=None)
+    assert result.status == "fail"
+    result, _, _ = data_access(
+        pair, get=get, head=lambda *a, **k: _Response(200), token="t"
+    )
+    assert result.status == "fail" and "without credentials" in result.summary
+    result, _, _ = data_access(
+        pair,
+        get=get,
+        head=lambda url, headers=None, **k: _Response(
+            404
+            if headers and "frs_2024_25.h5" in url and "enhanced" not in url
+            else (200 if headers else 401)
+        ),
+        token="t",
+    )
+    assert result.status == "fail" and "frs_2024_25 returned HTTP 404" in result.summary
+
+
+def _tiny_dataset(path: Path, year=2024, extra_person=None):
+    person = pd.DataFrame(
+        {
+            "person_id": [1, 2],
+            "age": [30.0, 5.0],
+            "employment_income": [30000.0, 0.0],
+            **(extra_person or {}),
+        }
+    )
+    benunit = pd.DataFrame({"benunit_id": [1], "would_claim_uc": [True]})
+    household = pd.DataFrame(
+        {"household_id": [1], "household_weight": [100.0], "rent": [0.0]}
+    )
+    with pd.HDFStore(path, mode="w") as store:
+        store.put("person", person)
+        store.put("benunit", benunit)
+        store.put("household", household)
+        store.put("time_period", pd.Series([str(year)]))
+    return path
+
+
+def test_dataset_bytes(tmp_path):
+    path = _tiny_dataset(tmp_path / "data.h5")
+    digest = hashlib.sha256(path.read_bytes()).hexdigest()
+    pair = make_pair(dataset_sha=digest)
+    pair = pair.model_copy(
+        update={
+            "data": pair.data.model_copy(update={"size_bytes": path.stat().st_size})
+        }
+    )
+    assert dataset_bytes(pair, path)[0].status == "pass"
+    assert dataset_bytes(make_pair(), path)[0].status == "fail"
+
+
+@pytest.mark.parametrize(
+    ("projected", "previous", "status"),
+    [
+        (list(range(2024, 2032)), list(range(2024, 2031)), "pass"),
+        (list(range(2024, 2031)), list(range(2024, 2031)), "needs_review"),
+        (list(range(2024, 2030)), list(range(2024, 2031)), "fail"),
+        (list(range(2026, 2031)), None, "fail"),
+        ([2020, 2021], None, "fail"),
+    ],
+)
+def test_supported_years(tmp_path, projected, previous, status):
+    path = _tiny_dataset(tmp_path / "data.h5", year=max(projected[0], 2024))
+    result, evidence = supported_years(
+        make_pair(),
+        path,
+        required_years=list(range(2025, 2031)),
+        budget_horizon_years=list(range(2026, 2032)),
+        previous_supported_years=previous,
+        projected_years=lambda _: projected,
+    )
+    assert result.status == status, result.summary
+
+
+# --- Budget inputs -------------------------------------------------------------------
+
+INPUT_NAMES = [
+    "age",
+    "employment_income",
+    "rent",
+    "would_claim_uc",
+    "household_weight",
+    "new_input_a",
+    "new_input_b",
+]
+
+
+def _tables(columns):
+    tables = {
+        "person": pd.DataFrame({"age": [30.0, 5.0], "employment_income": [3.0e4, 0.0]})
+    }
+    tables["household"] = pd.DataFrame(
+        {"household_weight": [1.0, 2.0], "rent": [0.0, 9.0]}
+    )
+    tables["benunit"] = pd.DataFrame({"would_claim_uc": [True, False]})
+    for entity in tables:
+        tables[entity] = tables[entity][
+            [c for c in tables[entity].columns if c in columns]
+        ]
+    return tables
+
+
+@settings(max_examples=60, deadline=None)
+@given(
+    present=st.sets(st.sampled_from(INPUT_NAMES[:5]), min_size=1),
+    previous=st.sets(st.sampled_from(INPUT_NAMES)),
+)
+def test_newly_missing_is_missing_now_minus_missing_before(present, previous):
+    result, evidence = budget_inputs(
+        make_pair(),
+        Path("unused.h5"),
+        model_input_variables=INPUT_NAMES,
+        critical_inputs=[],
+        accepted_gaps={},
+        previous_missing_inputs=sorted(previous),
+        previous_unusable_critical=[],
+        tables=_tables(present),
+    )
+    missing = sorted(set(INPUT_NAMES) - present)
+    assert evidence["missing_inputs"] == missing
+    assert evidence["newly_missing_inputs"] == sorted(set(missing) - previous)
+
+
+def test_critical_input_regression_fails_and_standing_gaps_need_review():
+    common = dict(
+        model_input_variables=INPUT_NAMES,
+        accepted_gaps={},
+        previous_missing_inputs=None,
+    )
+    result, _ = budget_inputs(
+        make_pair(),
+        Path("x"),
+        critical_inputs=["rent", "age"],
+        previous_unusable_critical=[],
+        tables=_tables(
+            {"age", "employment_income", "household_weight", "would_claim_uc"}
+        ),
+        **common,
+    )
+    assert result.status == "fail" and "rent (missing)" in result.summary
+    degenerate = _tables(set(INPUT_NAMES))
+    degenerate["household"]["rent"] = 0.0
+    result, _ = budget_inputs(
+        make_pair(),
+        Path("x"),
+        critical_inputs=["rent"],
+        previous_unusable_critical=[],
+        tables=degenerate,
+        **common,
+    )
+    assert result.status == "fail" and "degenerate" in result.summary
+    result, _ = budget_inputs(
+        make_pair(),
+        Path("x"),
+        critical_inputs=["rent"],
+        previous_unusable_critical=["rent"],
+        tables=degenerate,
+        **common,
+    )
+    assert result.status == "needs_review"
+    result, _ = budget_inputs(
+        make_pair(),
+        Path("x"),
+        critical_inputs=["rent"],
+        previous_unusable_critical=["rent"],
+        tables=degenerate,
+        **{**common, "accepted_gaps": {"rent": "uk-data#999"}},
+    )
+    assert result.status == "pass"
+
+
+def test_critical_inputs_file_lists_real_model_inputs():
+    import json
+
+    from policyengine_uk.system import system
+
+    path = (
+        Path(gates_module.__file__).resolve().parents[1]
+        / "data"
+        / "qualification"
+        / "uk_budget_critical_inputs.json"
+    )
+    critical = json.loads(path.read_text())["critical"]
+    not_inputs = [
+        name
+        for name in critical
+        if name not in system.variables
+        or not system.variables[name].is_input_variable()
+    ]
+    assert not_inputs == []
+
+
+# --- Reform contract -----------------------------------------------------------------
+
+
+def _declared_coverage(source):
+    """ROUTES, CASES and PROPERTIES as a runner module declares them, read
+    from a report of a full run (each case limited to the routes it ran on)."""
+
+    source = source or {}
+    from policyengine.tax_benefit_models.uk import budget_reform_contract as runner
+
+    declared_checks = {}
+    for case in runner.CASES:
+        for check in case.checks:
+            declared_checks.setdefault(
+                (case.id, check.target, check.name, check.year), []
+            ).append(check)
+    by_case = {}
+    for row in source.get("cases") or []:
+        by_case.setdefault(row.get("case_id"), []).append(row)
+    return {
+        "ROUTES": tuple(source.get("routes") or ()),
+        "CASES": tuple(
+            SimpleNamespace(
+                id=case,
+                routes=tuple(dict.fromkeys(row.get("route") for row in rows)),
+                checks=tuple(
+                    SimpleNamespace(
+                        target=label.split(":")[0],
+                        name=label.split(":")[1],
+                        year=int(label.split(":")[2]),
+                        expected=getattr(declaration, "expected", None),
+                        oracle=getattr(declaration, "oracle", None),
+                        pending=getattr(declaration, "pending", None)
+                        if declaration
+                        else next(
+                            (
+                                row.get("decision_id")
+                                for row in rows
+                                if row.get("check") == label
+                            ),
+                            None,
+                        ),
+                    )
+                    for label in dict.fromkeys(row.get("check") for row in rows)
+                    if label
+                    and len(label.split(":")) == 3
+                    and label.split(":")[0]
+                    not in ("baseline_isolation", "unreformed_after_reform")
+                    for declaration in declared_checks.get(
+                        (case, *label.split(":")[:2], int(label.split(":")[2])),
+                        [None],
+                    )
+                ),
+                expect_error="rejected"
+                if any(row.get("check") == "rejected" for row in rows)
+                else None,
+            )
+            for case, rows in by_case.items()
+            if case != "module_parameters_unchanged"
+        ),
+        "PROPERTIES": {
+            row.get("property_id"): "" for row in source.get("properties") or []
+        },
+        "UNSUPPORTED_ROUTES": {
+            row["route"]: row.get("reason", "")
+            for row in source.get("cases") or []
+            if row.get("status") == "unsupported"
+            and row.get("check") is None
+            and all(
+                other.get("status") == "unsupported" and other.get("check") is None
+                for other in source.get("cases") or []
+                if other.get("route") == row["route"]
+            )
+        },
+        "PENDING_DECISIONS": {
+            row["decision_id"]
+            for row in source.get("cases") or []
+            if row.get("decision_id")
+        },
+        "POPULATION_CHECK_ROUTES": tuple(
+            (row["check_id"], row["route"]) for row in source.get("population") or []
+        ),
+    }
+
+
+def _importer(report=None, raises=None, missing=False, coverage=None, declare=True):
+    """A runner module returning ``report``.
+
+    It declares the coverage of ``coverage`` (a report of a full run), by
+    default ``report`` itself, so a test of row statuses is not also a test
+    of coverage.
+    """
+
+    def runner(**kwargs):
+        if raises:
+            raise raises
+        return report
+
+    def importer(name):
+        if missing:
+            raise ModuleNotFoundError(name)
+        declared = (
+            _declared_coverage(coverage if coverage is not None else report)
+            if declare
+            else {}
+        )
+        return SimpleNamespace(run_budget_reform_contract=runner, **declared)
+
+    return importer
+
+
+ENTRY = "policyengine.tax_benefit_models.uk.budget_reform_contract:run_budget_reform_contract"
+
+
+def _run_level_row():
+    return {
+        "case_id": "module_parameters_unchanged",
+        "route": "all",
+        "check": "every parameter's dated values and metadata in system",
+        "status": "pass",
+    }
+
+
+@pytest.mark.parametrize(
+    ("kwargs", "status"),
+    [
+        ({"missing": True}, "unavailable"),
+        ({"raises": RuntimeError("boom")}, "error"),
+        (
+            {
+                "report": {
+                    "contract_version": "1.0.0",
+                    "cases": [
+                        {
+                            "case_id": "april",
+                            "route": "wrapper.policy",
+                            "status": "pass",
+                        },
+                        _run_level_row(),
+                    ],
+                }
+            },
+            "pass",
+        ),
+        (
+            {
+                "report": {
+                    "cases": [
+                        {
+                            "case_id": "april",
+                            "route": "country.reform_dict",
+                            "status": "fail",
+                        }
+                    ]
+                }
+            },
+            "fail",
+        ),
+        (
+            {
+                "report": {
+                    "cases": [
+                        {
+                            "case_id": "api",
+                            "route": "api.sim_api",
+                            "status": "unsupported",
+                        }
+                    ]
+                }
+            },
+            "error",
+        ),
+        (
+            {
+                "report": {
+                    "cases": [
+                        {
+                            "case_id": "api",
+                            "route": "api.sim_api",
+                            "status": "unsupported",
+                            "reason": "runs in sim-api",
+                        },
+                        {
+                            "case_id": "freeze",
+                            "route": "wrapper.policy",
+                            "check": "variable:income_tax:2027",
+                            "status": "pending",
+                            "decision_id": "d1344",
+                        },
+                        {
+                            "case_id": "freeze",
+                            "route": "wrapper.policy",
+                            "check": "unreformed_after_reform:income_tax:2027",
+                            "status": "pass",
+                        },
+                        _run_level_row(),
+                    ]
+                }
+            },
+            "pass",
+        ),
+        ({"report": {"cases": []}}, "error"),
+        (
+            {
+                "report": {
+                    "cases": [{"case_id": "x", "route": "r", "status": "pass"}],
+                    "properties": [
+                        {"property_id": "baseline_isolation", "status": "fail"}
+                    ],
+                }
+            },
+            "fail",
+        ),
+    ],
+)
+def test_reform_contract_statuses(kwargs, status):
+    result, _ = reform_contract(
+        entrypoint=ENTRY,
+        dataset_path=None,
+        seed=0,
+        max_examples=5,
+        importer=_importer(**kwargs),
+    )
+    assert result.status == status, result.summary
+
+
+def test_no_contract_configured_is_unavailable():
+    assert (
+        reform_contract(entrypoint=None, dataset_path=None, seed=0, max_examples=1)[
+            0
+        ].status
+        == "unavailable"
+    )
+
+
+CONTRACT_FIXTURES = Path(__file__).parent / "fixtures" / "uk_contract"
+CERTIFIED_SHA = "056e80fd8e9151d78f432a25cde863340baee81304c6216a37fb575ce8563784"
+STATUS_RANK = {"pass": 0, "error": 1, "fail": 2}
+
+
+def _fixture(name: str) -> dict:
+    return json.loads((CONTRACT_FIXTURES / name).read_text())
+
+
+def _recount(report: dict) -> dict:
+    """Make a hand-edited report's summary and holds match its rows again."""
+
+    summary = dict.fromkeys(("pass", "fail", "error", "unsupported", "pending"), 0)
+    for section in ("cases", "properties", "population"):
+        for row in report.get(section) or []:
+            summary[row["status"]] += 1
+    report["summary"] = summary
+    report["holds"] = summary["fail"] == 0 and summary["error"] == 0
+    return report
+
+
+def _passing_report() -> dict:
+    """A real full run of the runner: every route, 20 examples, the certified
+    pair and dataset."""
+
+    return _fixture("full_released_uk_2.130.1.json")
+
+
+def _run_contract(
+    report, *, versions=None, dataset_sha=CERTIFIED_SHA, coverage=None, **kwargs
+):
+    """Gate ``report`` against the coverage of W1's real full run (or ``coverage``)."""
+
+    kwargs.setdefault("seed", 0)
+    kwargs.setdefault("max_examples", 20)
+    kwargs.setdefault("dataset_path", Path("enhanced_frs_2024_25.h5"))
+    return reform_contract(
+        entrypoint=ENTRY,
+        importer=_importer(
+            report=report,
+            coverage=coverage if coverage is not None else _passing_report(),
+        ),
+        expected_versions=versions or _passing_report()["versions"],
+        expected_dataset_sha256=dataset_sha,
+        **kwargs,
+    )[0]
+
+
+def _run_released_contract(report, *, pending_decisions=()):
+    """Gate a fixture through the real runner's declarations and route logic."""
+
+    return _released_contract_outcome(report, pending_decisions=pending_decisions)[0]
+
+
+def _released_contract_outcome(report, *, pending_decisions=()):
+    """``(result, evidence)`` of ``_run_released_contract``."""
+    import importlib
+    import types
+
+    from policyengine.tax_benefit_models.uk import budget_reform_contract as runner
+
+    if runner.CONTRACT_VERSION != report["contract_version"]:
+        # Round 9 of #574: skipping hid a gate that rejected every real run.
+        pytest.fail(
+            f"the runner declares contract {runner.CONTRACT_VERSION}, the fixture "
+            f"{report['contract_version']}: refresh tests/fixtures/uk_contract "
+            "with a real full run of the runner"
+        )
+    proxy = types.ModuleType(runner.__name__)
+    proxy.__dict__.update(vars(runner))
+    proxy.run_budget_reform_contract = lambda **kwargs: report
+
+    def importer(name):
+        return proxy if name == runner.__name__ else importlib.import_module(name)
+
+    return reform_contract(
+        entrypoint=ENTRY,
+        importer=importer,
+        expected_versions=report["versions"],
+        expected_dataset_sha256=CERTIFIED_SHA,
+        seed=0,
+        max_examples=20,
+        dataset_path=Path("enhanced_frs_2024_25.h5"),
+        pending_decisions=pending_decisions,
+    )
+
+
+def test_the_released_models_full_run_passes_the_runners_own_declarations():
+    """W1's full run remains valid against the real runner's declarations."""
+
+    result = _run_released_contract(_fixture("full_released_uk_2.130.1.json"))
+    assert result.status == "pass", result.summary
+    assert result.details["counts"] == RELEASED_COUNTS
+
+
+RELEASED_COUNTS = {
+    "pass": 792,
+    "fail": 0,
+    "error": 0,
+    "unsupported": 101,
+    "pending": 25,
+}
+
+
+def test_the_released_run_without_assertion_ids_resolves_by_oracle_and_order():
+    """Round 10 of #574: every row of the 1.1.0 run has an assertion_id, so
+    the gate's resolution of rows without one (by oracle, then order) no
+    longer ran on real data, and three mutants of it survived."""
+
+    report = _fixture("full_released_uk_2.130.1.json")
+    for row in report["cases"]:
+        row.pop("assertion_id", None)
+    result = _run_released_contract(report)
+    assert result.status == "pass", result.summary
+    assert result.details["counts"] == RELEASED_COUNTS
+
+
+def _population_leaves(value):
+    if isinstance(value, dict):
+        for item in value.values():
+            yield from _population_leaves(item)
+    elif isinstance(value, list):
+        for item in value:
+            yield from _population_leaves(item)
+    else:
+        yield value
+
+
+@pytest.mark.parametrize("outcome", ["pass", "fail", "error"])
+@pytest.mark.parametrize(
+    "variant", ["small counts", "routes differ", "numpy", "text and decimal"]
+)
+def test_the_ledger_publishes_no_value_from_the_survey_data(outcome, variant):
+    """Rounds 10, supp-r3 and supp-r4 of #574: population rows are computed on
+    the survey data, and their numbers combine (rows of one check on
+    different routes share a subsample, so their difference is a handful's),
+    so the published evidence keeps only each detail's shape and true/false
+    findings, on a pass, a fail and an error alike. Its digest is the digest
+    recorded, never one of the full report. Values synthetic."""
+
+    from decimal import Decimal
+
+    import numpy as np
+
+    from policyengine.provenance.uk_candidate import WITHHELD_VALUE
+
+    report = _fixture("full_released_uk_2.130.1.json")
+    april = [
+        r
+        for r in report["population"]
+        if r["check_id"] == "population_april_rate_change"
+    ]
+    for index, row in enumerate(april):
+        row["detail"] = {
+            "2026_unchanged": True,
+            "2027_total_change": 205573.0
+            + (index * 4321.0 if variant == "routes differ" else 0),
+            "2027_people_with_lower_tax": {
+                "small counts": 3,
+                "routes differ": 20,
+                "numpy": np.int64(4),
+                "text and decimal": "4",
+            }[variant],
+            "2027_people_with_higher_tax": (
+                Decimal(1093) if variant == "text and decimal" else 1093 + index * 4
+            ),
+            "nested": [{"count": 5, "ok": False}],
+            "names": ["income_tax:2027", "income_tax:2028"],
+            "total_nan": float("nan"),
+        }
+    if outcome == "fail":
+        case = next(r for r in report["cases"] if r["status"] == "pass")
+        case["status"] = "fail"
+        report = _recount(report)
+    if outcome == "error":
+        report["seed"] = 1
+    result, evidence = _released_contract_outcome(report)
+    assert result.status == outcome, result.summary
+    for row in evidence["population"]:
+        for leaf in _population_leaves(row.get("detail")):
+            assert leaf is None or isinstance(leaf, bool) or leaf == WITHHELD_VALUE, (
+                row["check_id"],
+                leaf,
+            )
+    published = [
+        r["detail"]
+        for r in evidence["population"]
+        if r["check_id"] == "population_april_rate_change"
+    ]
+    assert published[0] == {
+        "2026_unchanged": True,
+        "2027_total_change": WITHHELD_VALUE,
+        "2027_people_with_lower_tax": WITHHELD_VALUE,
+        "2027_people_with_higher_tax": WITHHELD_VALUE,
+        # A nested list or mapping is withheld whole: its length or keys
+        # could be a count (supp-r5), and a NaN reads "withheld" too (P12).
+        "nested": WITHHELD_VALUE,
+        "names": WITHHELD_VALUE,
+        "total_nan": WITHHELD_VALUE,
+    }
+    # Every field but the detail is published as reported (supp-r5 C04).
+    assert [
+        {k: v for k, v in r.items() if k != "detail"} for r in evidence["population"]
+    ] == [{k: v for k, v in r.items() if k != "detail"} for r in report["population"]]
+    assert {k: v for k, v in evidence.items() if k != "population"} == {
+        k: v for k, v in report.items() if k != "population"
+    }
+    assert result.details["report_sha256"] == gates_module.evidence_sha256(evidence)
+    assert result.evidence_sha256 == gates_module.evidence_sha256(evidence)
+
+
+@settings(max_examples=200, deadline=None)
+@given(
+    detail=st.recursive(
+        st.one_of(
+            st.integers(-20, 2000),
+            st.booleans(),
+            st.floats(allow_nan=False, allow_infinity=False),
+            st.none(),
+            st.text(max_size=5),
+        ),
+        lambda children: (
+            st.lists(children, max_size=3)
+            | st.dictionaries(st.text(max_size=5), children, max_size=3)
+        ),
+        max_leaves=10,
+    )
+)
+def test_only_true_false_findings_survive_publication(detail):
+    from policyengine.provenance.uk_candidate import (
+        WITHHELD_VALUE,
+        publishable_contract_report,
+    )
+
+    report = {"cases": [{"detail": detail}], "population": [{"detail": detail}]}
+    published = publishable_contract_report(report)
+    # Synthetic case rows are left alone.
+    assert published["cases"] == report["cases"]
+
+    def finding(value):
+        return value if value is None or isinstance(value, bool) else WITHHELD_VALUE
+
+    expected = (
+        {key: finding(value) for key, value in detail.items()}
+        if isinstance(detail, dict)
+        else finding(detail)
+    )
+    assert published["population"][0]["detail"] == expected
+
+
+def test_population_rows_publish_only_the_runners_fixed_fields():
+    """supp-r5 C04 and C07: the runner's fixed fields, an error row's status
+    among them, are published as reported; any other field is withheld,
+    true/false ones included (the confirmation review: an unknown field's
+    name is not the runner's, so its finding is not published), and an
+    error row's text in its detail too. Values synthetic."""
+
+    from policyengine.provenance.uk_candidate import (
+        WITHHELD_VALUE,
+        publishable_contract_report,
+    )
+
+    fixed = {
+        "check_id": "population_april_rate_change",
+        "route": "country.reform_dict",
+        "status": "error",
+        "reason": None,
+        "seed": 0,
+        "subsample_households": 1000,
+        "reform_identity": None,
+        "reform_identity_unavailable": "population check rows are not bound to a reform identity",
+    }
+    row = {
+        **fixed,
+        "detail": "RuntimeError: total 1234.5 over 4 households\n",
+        "total_change": 1234.5,
+        "households": ["h1", "h2"],
+        "flag": True,
+    }
+    (published,) = publishable_contract_report({"population": [row]})["population"]
+    assert published == {
+        **fixed,
+        "detail": WITHHELD_VALUE,
+        "total_change": WITHHELD_VALUE,
+        "households": WITHHELD_VALUE,
+        "flag": WITHHELD_VALUE,
+    }
+
+
+@pytest.mark.parametrize("section", ["cases", "population"])
+def test_supported_failures_cannot_be_relabelled_unsupported(section):
+    report = _fixture("full_released_uk_2.130.1.json")
+    row = next(
+        row
+        for row in report[section]
+        if row["status"] == "pass" and row["route"] == "country.reform_dict"
+    )
+    row["status"] = "fail"
+    assert _run_released_contract(_recount(report)).status == "fail"
+    row.update(status="unsupported", reason="this candidate does not support it")
+    result = _run_released_contract(_recount(report))
+    assert result.status == "error", result.summary
+    assert any(
+        "undeclared unsupported" in problem for problem in result.details["problems"]
+    )
+
+
+def test_population_cannot_exempt_every_required_check():
+    report = _fixture("full_released_uk_2.130.1.json")
+    assert len(report["population"]) == 19
+    for row in report["population"]:
+        row.update(status="unsupported", reason="no population support")
+    result = _run_released_contract(_recount(report))
+    assert result.status == "error", result.summary
+    assert (
+        len([p for p in result.details["problems"] if "undeclared unsupported" in p])
+        == 19
+    )
+
+
+@pytest.mark.parametrize("row_index", range(19))
+def test_each_population_route_row_is_required(row_index):
+    report = _fixture("full_released_uk_2.130.1.json")
+    omitted = report["population"].pop(row_index)
+    result = _run_released_contract(_recount(report))
+    assert result.status == "error", result.summary
+    assert any(
+        f"{omitted['check_id']}@{omitted['route']}" in problem
+        for problem in result.details["problems"]
+    )
+
+
+def test_deleting_a_failed_uc_population_route_cannot_qualify():
+    report = _fixture("full_released_uk_2.130.1.json")
+    omitted = next(
+        row
+        for row in report["population"]
+        if row["check_id"] == "population_uc_standard_allowance_plus_10"
+        and row["route"] == "country.scenario_parameter_changes"
+    )
+    omitted["status"] = "fail"
+    assert _run_released_contract(_recount(report)).status == "fail"
+    report["population"].remove(omitted)
+    assert _run_released_contract(_recount(report)).status == "error"
+
+
+@pytest.mark.parametrize("override", [(), ("d1344",)])
+@pytest.mark.parametrize(
+    "check_id",
+    [
+        "population_query_order",
+        "population_uc_standard_allowance_plus_10",
+        "population_state_pension_plus_10",
+    ],
+)
+def test_population_assertion_cannot_borrow_a_case_pending_decision(override, check_id):
+    report = _fixture("full_released_uk_2.130.1.json")
+    row = next(row for row in report["population"] if row["check_id"] == check_id)
+    row["status"] = "fail"
+    assert _run_released_contract(_recount(report)).status == "fail"
+    row.update(status="pending", decision_id="d1344")
+    result = _run_released_contract(_recount(report), pending_decisions=override)
+    assert result.status == "error", result.summary
+    assert any(
+        "population checks cannot be pending" in p for p in result.details["problems"]
+    )
+
+
+def test_one_population_row_per_check_id_cannot_qualify():
+    report = _fixture("full_released_uk_2.130.1.json")
+    report["population"] = list(
+        {row["check_id"]: row for row in report["population"]}.values()
+    )
+    assert len(report["population"]) == 7
+    assert _run_released_contract(_recount(report)).status == "error"
+
+
+def test_duplicate_population_route_cannot_replace_another_route():
+    report = _fixture("full_released_uk_2.130.1.json")
+    report["population"][1] = dict(report["population"][0])
+    assert _run_released_contract(_recount(report)).status == "error"
+
+
+def test_an_unknown_assertion_identity_cannot_qualify():
+    report = _fixture("full_released_uk_2.130.1.json")
+    row = next(
+        row
+        for row in report["cases"]
+        if row["status"] == "pass" and row.get("oracle") == "frozen"
+    )
+    row["assertion_id"] = "an assertion the runner does not declare"
+    result = _run_released_contract(report)
+    assert result.status == "error", result.summary
+    assert any(
+        "no unique declared assertion identity" in p for p in result.details["problems"]
+    )
+
+
+@pytest.mark.parametrize("override", [(), ("d1344",)])
+def test_mandatory_freeze_assertion_cannot_borrow_pending_authorization(override):
+    report = _fixture("full_released_uk_2.130.1.json")
+    row = next(
+        row
+        for row in report["cases"]
+        if row["case_id"] == "extended_freeze"
+        and row["route"] == "country.reform_dict"
+        and row.get("check")
+        == "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034"
+        # The mandatory one of the two 2034 checks (the other awaits d1344).
+        and row["status"] == "pass"
+        and row.get("decision_id") is None
+    )
+    row["status"] = "fail"
+    assert _run_released_contract(_recount(report)).status == "fail"
+    row.update(status="pending", decision_id="d1344")
+    result = _run_released_contract(_recount(report), pending_decisions=override)
+    assert result.status == "error", result.summary
+    assert any("mandatory assertion" in p for p in result.details["problems"])
+
+
+def test_swapping_mandatory_and_pending_freeze_statuses_cannot_qualify():
+    report = _fixture("full_released_uk_2.130.1.json")
+    rows = [
+        row
+        for row in report["cases"]
+        if row["case_id"] == "extended_freeze"
+        and row["route"] == "country.reform_dict"
+        and row.get("check")
+        == "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034"
+    ]
+    assert len(rows) == 2
+    mandatory = next(row for row in rows if row["status"] == "pass")
+    pending = next(row for row in rows if row["status"] == "pending")
+    mandatory.update(status="pending", decision_id="d1344")
+    pending.update(status="pass", decision_id=None)
+    assert _run_released_contract(_recount(report)).status == "error"
+
+
+@pytest.mark.parametrize("replace_status", ["pass", "pending"])
+def test_duplicate_freeze_assertion_cannot_satisfy_the_other_category(replace_status):
+    report = _fixture("full_released_uk_2.130.1.json")
+    matching = [
+        row
+        for row in report["cases"]
+        if row["case_id"] == "extended_freeze"
+        and row["route"] == "country.reform_dict"
+        and row.get("check")
+        == "parameter:gov.hmrc.income_tax.allowances.personal_allowance.amount:2034"
+    ]
+    source = next(row for row in matching if row["status"] == replace_status)
+    replaced = next(row for row in matching if row["status"] != replace_status)
+    report["cases"][report["cases"].index(replaced)] = dict(source)
+    result = _run_released_contract(_recount(report))
+    assert result.status == "error", result.summary
+    assert any(
+        "assertion multiplicities differ" in p for p in result.details["problems"]
+    )
+
+
+@pytest.mark.parametrize("route", ["api.sim_api", "wrapper.household"])
+def test_declared_case_exemptions_require_the_runners_reason(route):
+    report = _fixture("full_released_uk_2.130.1.json")
+    row = next(
+        row
+        for row in report["cases"]
+        if row["route"] == route and row["status"] == "unsupported"
+    )
+    row["reason"] = "a different exemption"
+    result = _run_released_contract(report)
+    assert result.status == "error", result.summary
+    assert any("different reason" in p for p in result.details["problems"])
+
+
+def test_the_runners_own_run_level_row_is_recognised():
+    """Round 9 of #574: contract 1.1.0 reworded the shared parameter check,
+    and a gate fixed to the 1.0.0 wording rejected every candidate. The row
+    the installed runner emits must pass; an unrelated check must not."""
+
+    from policyengine.tax_benefit_models.uk import budget_reform_contract as runner
+
+    def gated(current):
+        # The runner's whole row, so a renamed case or route fails too
+        # (round 10 of #574).
+        report = _passing_report()
+        index = next(
+            index
+            for index, row in enumerate(report["cases"])
+            if row["case_id"] == "module_parameters_unchanged"
+        )
+        report["cases"][index] = current
+        return _run_contract(report)
+
+    current = runner.module_parameters_unchanged(runner.shared_state())
+    assert current["status"] == "pass"
+    assert gated(current).status == "pass", gated(current).summary
+    unrelated = gated(
+        {**current, "check": "every case's reform digest in policyengine-uk"}
+    )
+    assert unrelated.status == "error"
+    assert any(
+        "does not identify the shared parameter check" in problem
+        for problem in unrelated.details["problems"]
+    )
+
+
+def test_deleting_a_failed_run_level_check_cannot_qualify():
+    report = _fixture("full_released_uk_2.130.1.json")
+    row = next(
+        row
+        for row in report["cases"]
+        if row["case_id"] == "module_parameters_unchanged"
+    )
+    row["status"] = "fail"
+    assert _run_released_contract(_recount(report)).status == "fail"
+    report["cases"].remove(row)
+    result = _run_released_contract(_recount(report))
+    assert result.status == "error", result.summary
+    assert any(
+        "module_parameters_unchanged@all" in p for p in result.details["problems"]
+    )
+
+
+@pytest.mark.parametrize(
+    ("route", "check_prefix"),
+    [
+        ("country.reform_dict", "baseline_isolation:"),
+        ("country.scenario_parameter_changes", "baseline_isolation:"),
+        ("country.reform_from_dict", "baseline_isolation:"),
+        ("country.reform_dict", "unreformed_after_reform:gov."),
+        ("country.reform_dict", "unreformed_after_reform:income_tax:"),
+        ("wrapper.household", "unreformed_after_reform:"),
+        ("wrapper.policy", "unreformed_after_reform:"),
+    ],
+)
+def test_each_runner_isolation_row_is_required(route, check_prefix):
+    report = _fixture("full_released_uk_2.130.1.json")
+    omitted = next(
+        row
+        for row in report["cases"]
+        if row["route"] == route and (row.get("check") or "").startswith(check_prefix)
+    )
+    report["cases"].remove(omitted)
+    result = _run_released_contract(_recount(report))
+    assert result.status == "error", result.summary
+    assert any(omitted["check"] in p for p in result.details["problems"])
+
+
+def test_deleting_all_runner_isolation_rows_cannot_qualify():
+    report = _fixture("full_released_uk_2.130.1.json")
+    report["cases"] = [
+        row
+        for row in report["cases"]
+        if not (row.get("check") or "").startswith(
+            ("baseline_isolation:", "unreformed_after_reform:")
+        )
+    ]
+    result = _run_released_contract(_recount(report))
+    assert result.status == "error", result.summary
+    assert any("baseline_isolation:" in p for p in result.details["problems"])
+
+
+@pytest.mark.parametrize(
+    "check_prefix", ["baseline_isolation:", "unreformed_after_reform:"]
+)
+def test_failing_isolation_checks_cannot_be_relabelled_pending(check_prefix):
+    report = _fixture("full_released_uk_2.130.1.json")
+    row = next(
+        row
+        for row in report["cases"]
+        if row["route"] == "country.reform_dict"
+        and (row.get("check") or "").startswith(check_prefix)
+    )
+    row["status"] = "fail"
+    assert _run_released_contract(_recount(report)).status == "fail"
+    row.update(status="pending", decision_id="d1344")
+    result = _run_released_contract(_recount(report), pending_decisions=("d1344",))
+    assert result.status == "error", result.summary
+    assert any(
+        "isolation check" in p and "did not run" in p
+        for p in result.details["problems"]
+    )
+
+
+def test_w1_report_on_the_pinned_model_fails_on_its_defects():
+    """W1's real report on policyengine-uk 2.125.1: 66 failures and 35 errors."""
+
+    report = _fixture("pinned_uk_2.125.1_defects.json")
+    result = _run_contract(
+        report,
+        versions={"policyengine-uk": "2.125.1", "policyengine-core": "3.33.0"},
+        dataset_sha=None,
+        dataset_path=None,
+        max_examples=report["max_examples"],
+        coverage=report,
+    )
+    assert result.status == "fail"
+    assert result.summary.startswith("101 contract row(s) failed")
+    # This historical runner predates the always-emitted run-level check and
+    # after-run isolation rows. Its failures remain decisive; current coverage
+    # validation also correctly identifies those absent rows.
+    assert any("declared case check(s)" in p for p in result.details["problems"])
+    assert any(
+        "module_parameters_unchanged@all" in p for p in result.details["problems"]
+    )
+
+
+def test_w1_population_run_alone_is_incomplete():
+    """W1's real population-only run has no cases and no sampled properties."""
+
+    population_only = _fixture("population_pass_max_examples_0.json")
+    result = _run_contract(population_only, max_examples=0, coverage=population_only)
+    assert result.status == "error"
+    problems = result.details["problems"]
+    assert "the report has no cases" in problems
+    assert any("annual_values_match_the_oracle did not run" in p for p in problems)
+
+
+def test_w1_full_run_passes():
+    """The real full run of contract 1.1.0 on the certified pair: 792 pass,
+    25 pending a methodology call, 101 unsupported by design."""
+
+    result = _run_contract(_passing_report())
+    assert result.status == "pass", result.summary
+    assert "every case, property and population check passed (792)" in result.summary
+    assert "25 pending a methodology call" in result.summary
+    assert result.details["problems"] == []
+
+
+def _drop(report, **sections):
+    """``report`` with rows removed, its summary and holds recounted."""
+
+    return _recount({**report, **sections})
+
+
+COVERAGE_GAPS = {
+    "each case reduced to one check per route": lambda r: _drop(
+        r,
+        cases=list(
+            {(row["case_id"], row["route"]): row for row in r["cases"]}.values()
+        ),
+    ),
+    "all but one population check": lambda r: _drop(r, population=r["population"][:1]),
+    "pending properties with invented decisions": lambda r: _drop(
+        r,
+        properties=[
+            {**row, "status": "pending", "decision_id": "invented", "examples_run": 0}
+            for row in r["properties"]
+        ],
+    ),
+    "one case on one route": lambda r: (
+        _drop(r, cases=r["cases"][:1], population=r["population"][:1], properties=[])
+        | {"routes": r["routes"][:1]}
+    ),
+    "a route": lambda r: _drop(
+        r, cases=[row for row in r["cases"] if row["route"] != "wrapper.policy"]
+    ),
+    "a route from the route list": lambda r: {**r, "routes": r["routes"][:-1]},
+    "a case on one route": lambda r: _drop(
+        r,
+        cases=[
+            row
+            for row in r["cases"]
+            if (row["case_id"], row["route"])
+            != ("extended_freeze", "wrapper.household")
+        ],
+    ),
+    "a sampled property": lambda r: _drop(r, properties=r["properties"][:1]),
+    "every sampled property": lambda r: _drop(r, properties=[]),
+    "the sampled examples": lambda r: {
+        **r,
+        "properties": [{**row, "examples_run": 0} for row in r["properties"]],
+    },
+    "most sampled examples": lambda r: {
+        **r,
+        "properties": [{**row, "examples_run": 19} for row in r["properties"]],
+    },
+}
+
+
+@pytest.mark.parametrize("name", sorted(COVERAGE_GAPS))
+def test_a_report_that_drops_coverage_does_not_qualify(name):
+    """Interrupted review of #574: a recounted partial report passed, as did
+    properties that passed on zero examples."""
+
+    report = COVERAGE_GAPS[name](copy.deepcopy(_passing_report()))
+    result = _run_contract(report)
+    assert result.status == "error", (name, result.summary)
+
+
+def test_a_runner_that_declares_no_coverage_cannot_qualify():
+    report = _passing_report()
+    result = reform_contract(
+        entrypoint=ENTRY,
+        dataset_path=Path("enhanced_frs_2024_25.h5"),
+        seed=0,
+        max_examples=20,
+        expected_versions={"policyengine-uk": "2.126.8", "policyengine-core": "3.33.0"},
+        expected_dataset_sha256=CERTIFIED_SHA,
+        importer=_importer(report=report, declare=False),
+    )[0]
+    assert result.status == "error"
+    assert "declares no ROUTES, CASES and PROPERTIES" in result.summary
+
+
+def test_population_coverage_uses_the_population_runners_declarations():
+    """A check added by the runner must be required without a gate-side list."""
+
+    report = _passing_report()
+    importer = _importer(report=report, coverage=report)
+    module = importer("contract")
+    del module.POPULATION_CHECK_ROUTES
+    population = SimpleNamespace()
+    population.__doc__ = (
+        "Checks:\n\n``new_population_check``\n    Declared by the population runner."
+    )
+    result = reform_contract(
+        entrypoint=ENTRY,
+        dataset_path=Path("dataset.h5"),
+        seed=0,
+        max_examples=20,
+        importer=lambda name: (
+            population if name.endswith("budget_reform_population") else module
+        ),
+    )[0]
+    assert result.status == "error"
+    assert "new_population_check" in result.summary
+
+
+def test_new_population_route_declaration_is_required_without_a_gate_side_list(
+    monkeypatch,
+):
+    from policyengine.tax_benefit_models.uk import (
+        budget_reform_population as population,
+    )
+
+    declared = population.population_check_routes
+    monkeypatch.setattr(
+        population,
+        "population_check_routes",
+        lambda routes: (*declared(routes), ("new_population_check", "unreformed")),
+    )
+    report = _fixture("full_released_uk_2.130.1.json")
+    result = _run_released_contract(report)
+    assert result.status == "error", result.summary
+    assert any(
+        "new_population_check@unreformed" in p for p in result.details["problems"]
+    )
+    report["population"].append(
+        {"check_id": "new_population_check", "route": "unreformed", "status": "pass"}
+    )
+    assert _run_released_contract(_recount(report)).status == "pass"
+
+
+REPORT_TAMPERING = {
+    "invented pending decision": lambda r: _recount(
+        {
+            **r,
+            "cases": [
+                {**row, "decision_id": "invented"}
+                if row["status"] == "pending"
+                else row
+                for row in r["cases"]
+            ],
+        }
+    ),
+    "other model": lambda r: r["versions"].update({"policyengine-uk": "2.125.1"}),
+    "other core": lambda r: r["versions"].update({"policyengine-core": "3.30.0"}),
+    "other dataset": lambda r: r["dataset"].update(sha256="0" * 64),
+    "no population": lambda r: _recount({**r, "population": []}),
+    "other seed": lambda r: r.update(seed=7),
+    "fewer examples": lambda r: r.update(max_examples=5),
+    "summary": lambda r: r["summary"].update(
+        unsupported=r["summary"]["unsupported"] + 1
+    ),
+    "holds": lambda r: r.update(holds=False),
+    "pending without decision": lambda r: _recount(
+        {**r, "cases": [{**r["cases"][0], "status": "pending"}, *r["cases"][1:]]}
+    ),
+    "unsupported without reason": lambda r: _recount(
+        {
+            **r,
+            "population": [
+                {**r["population"][0], "status": "unsupported"},
+                *r["population"][1:],
+            ],
+        }
+    ),
+}
+
+
+@pytest.mark.parametrize("name", sorted(REPORT_TAMPERING))
+def test_a_report_that_is_not_a_full_run_of_this_candidate_is_an_error(name):
+    report = copy.deepcopy(_passing_report())
+    tampered = REPORT_TAMPERING[name](report) or report
+    assert _run_contract(tampered).status == "error"
+
+
+@settings(max_examples=40, deadline=None)
+@given(
+    data=st.data(),
+    worse=st.sampled_from(["fail", "error"]),
+    section=st.sampled_from(["cases", "properties", "population"]),
+)
+def test_a_worse_row_never_improves_the_gate(data, worse, section):
+    """Monotone: turning any row into a failure fails the gate."""
+
+    report = _passing_report()
+    index = data.draw(st.integers(0, len(report[section]) - 1))
+    report[section][index] = {**report[section][index], "status": worse}
+    result = _run_contract(_recount(report))
+    assert STATUS_RANK[result.status] >= STATUS_RANK["fail"]
+
+
+# --- Metrics review ------------------------------------------------------------------
+
+BOUNDS = {
+    "default": {"relative": 0.02, "absolute": 5e8},
+    "kinds": {"poverty": {"relative": 0.0, "absolute": 0.005}},
+}
+
+
+#: The small-cell fields every row of the pinned tool's report carries.
+PUBLISHED = {"suppressed": False, "excludes": [], "exclude_signs": {}}
+
+
+def _report(values, **extra):
+    return {
+        "status": "ok",
+        "metrics_version": "m1",
+        "partition_version": "p1",
+        "metrics": [
+            {"metric_id": mid, "year": year, "value": value, "kind": kind, **PUBLISHED}
+            for (mid, year, kind), value in values.items()
+        ],
+        # Every real report carries checks; one always passes here.
+        "identities": [
+            {"identity_id": "weights:household", "status": "pass"},
+            *extra.pop("identities", []),
+        ],
+        **extra,
+    }
+
+
+BASE = {
+    ("income_tax", 2027, "receipt"): 3.0e11,
+    ("relative_poverty", 2027, "poverty"): 0.17,
+}
+
+
+def test_metric_comparison_rules():
+    assert compare_metric_reports(_report(BASE), _report(BASE), BOUNDS)[0] == "pass"
+    moved = {**BASE, ("income_tax", 2027, "receipt"): 3.1e11}
+    assert (
+        compare_metric_reports(_report(moved), _report(BASE), BOUNDS)[0]
+        == "needs_review"
+    )
+    assert (
+        compare_metric_reports(
+            _report(BASE, metrics_version="m2"), _report(BASE), BOUNDS
+        )[0]
+        == "fail"
+    )
+    assert (
+        compare_metric_reports(
+            _report(BASE, partition_version="p2"), _report(BASE), BOUNDS
+        )[0]
+        == "needs_review"
+    )
+    lost = {("income_tax", 2027, "receipt"): 3.0e11}
+    assert compare_metric_reports(_report(lost), _report(BASE), BOUNDS)[0] == "fail"
+    nan = {**BASE, ("income_tax", 2027, "receipt"): float("nan")}
+    assert compare_metric_reports(_report(nan), _report(BASE), BOUNDS)[0] == "fail"
+    warned = _report(
+        BASE, identities=[{"identity_id": "inert:alcohol_duty", "status": "warn"}]
+    )
+    # A warning the previous report also gave is recorded; any other is new
+    # (the tool's new_warning), including one from a check it did not run.
+    status, _, evidence = compare_metric_reports(warned, warned, BOUNDS)
+    assert status == "pass" and evidence["warn_identities"] == ["inert:alcohol_duty"]
+    status, _, evidence = compare_metric_reports(warned, _report(BASE), BOUNDS)
+    assert status == "needs_review"
+    assert evidence["new_warnings"] == ["inert:alcohol_duty"]
+
+
+def test_reports_that_measure_nothing_fail():
+    """Interrupted review of #574: two empty reports compared as a pass."""
+
+    assert compare_metric_reports({}, {}, BOUNDS)[0] == "fail"
+    full = _report(BASE)
+    for broken in (
+        {**full, "metrics": []},
+        {**full, "identities": []},
+        {**full, "status": "error"},
+        {key: value for key, value in full.items() if key != "status"},
+    ):
+        assert compare_metric_reports(broken, full, BOUNDS)[0] == "fail"
+        assert compare_metric_reports(full, broken, BOUNDS)[0] == "fail"
+
+
+@pytest.mark.parametrize("status", ["error", "unsupported", "pending", None, "PASS"])
+def test_a_check_with_an_unknown_status_is_not_a_pass(status):
+    candidate = _report(BASE, identities=[{"identity_id": "closure", "status": status}])
+    previous = _report(BASE, identities=[{"identity_id": "closure", "status": "pass"}])
+    result = compare_metric_reports(candidate, previous, BOUNDS)
+    assert result[0] == "fail" and "unknown status" in result[1]
+
+
+def test_a_check_that_disappears_is_lost_coverage():
+    previous = _report(
+        BASE,
+        identities=[
+            {"identity_id": "closure", "status": "pass"},
+            {"identity_id": "sign:vat", "status": "pass"},
+        ],
+    )
+    # A hard check that vanished under the same partition fails.
+    no_closure = _report(
+        BASE, identities=[{"identity_id": "sign:vat", "status": "pass"}]
+    )
+    result = compare_metric_reports(no_closure, previous, BOUNDS)
+    assert result[0] == "fail" and "checks disappeared: closure" in result[1]
+    # A no-worsening check that vanished needs a look.
+    no_sign = _report(BASE, identities=[{"identity_id": "closure", "status": "pass"}])
+    assert compare_metric_reports(no_sign, previous, BOUNDS)[0] == "needs_review"
+    # When the model's partition changed, vanished per-head checks need review.
+    repartitioned = _report(
+        BASE,
+        partition_version="p2",
+        identities=[{"identity_id": "sign:vat", "status": "pass"}],
+    )
+    assert compare_metric_reports(repartitioned, previous, BOUNDS)[0] == "needs_review"
+
+
+def _checks(**statuses):
+    return [
+        {"identity_id": key.replace("__", ":"), "status": value}
+        for key, value in statuses.items()
+    ]
+
+
+@pytest.mark.parametrize(
+    ("candidate", "previous", "status"),
+    [
+        # Hard checks fail whatever the previous pair did.
+        ({"finite__vat": "fail"}, {"finite__vat": "fail"}, "fail"),
+        ({"closure": "fail"}, {}, "fail"),
+        (
+            {"identity__partition_sum": "fail"},
+            {"identity__partition_sum": "pass"},
+            "fail",
+        ),
+        ({"determinism__repeat": "fail"}, {}, "fail"),
+        # Unknown check kinds are treated as hard: no silent skips.
+        ({"newkind__x": "fail"}, {"newkind__x": "fail"}, "fail"),
+        # Known-defect checks fail only as regressions.
+        (
+            {"counted_once__statutory_sick_pay": "fail"},
+            {"counted_once__statutory_sick_pay": "fail"},
+            "pass",
+        ),
+        (
+            {"counted_once__statutory_sick_pay": "fail"},
+            {"counted_once__statutory_sick_pay": "pass"},
+            "fail",
+        ),
+        ({"sign__vat": "fail"}, {}, "fail"),
+        ({"bounds__uc": "fail"}, {"bounds__uc": "fail"}, "pass"),
+        # The overlap guard fails on main for a known defect: no-worsening.
+        (
+            {"overlap_guard__property_transaction_tax_geography": "fail"},
+            {"overlap_guard__property_transaction_tax_geography": "fail"},
+            "pass",
+        ),
+        (
+            {"overlap_guard__property_transaction_tax_geography": "fail"},
+            {"overlap_guard__property_transaction_tax_geography": "pass"},
+            "fail",
+        ),
+        # A check skipped before that fails now has worsened.
+        (
+            {"counted_once__statutory_sick_pay": "fail"},
+            {"counted_once__statutory_sick_pay": "skipped"},
+            "fail",
+        ),
+        # Skipped is neither a pass nor a failure, but losing a passing
+        # check needs a look.
+        (
+            {"counted_once__winter_fuel": "skipped"},
+            {"counted_once__winter_fuel": "skipped"},
+            "pass",
+        ),
+        (
+            {"counted_once__winter_fuel": "skipped"},
+            {"counted_once__winter_fuel": "pass"},
+            "needs_review",
+        ),
+        # The reform checks are hard, whatever the previous pair did.
+        (
+            {"responsive__state_pension_plus_10pc": "fail"},
+            {"responsive__state_pension_plus_10pc": "fail"},
+            "fail",
+        ),
+        (
+            {"reform_runs__uc_taper_20pc": "fail"},
+            {"reform_runs__uc_taper_20pc": "fail"},
+            "fail",
+        ),
+        # Warnings are recorded; a check that newly warns needs review.
+        ({"inert__tobacco_duty": "warn"}, {"inert__tobacco_duty": "warn"}, "pass"),
+        (
+            {"inert__tobacco_duty": "warn"},
+            {"inert__tobacco_duty": "pass"},
+            "needs_review",
+        ),
+        (
+            {"inert__tobacco_duty": "warn"},
+            {"inert__tobacco_duty": "skipped"},
+            "needs_review",
+        ),
+        (
+            {"inert__tobacco_duty": "warn"},
+            {"inert__tobacco_duty": "fail"},
+            "needs_review",
+        ),
+    ],
+)
+def test_check_classification(candidate, previous, status):
+    result = compare_metric_reports(
+        _report(BASE, identities=_checks(**candidate)),
+        _report(BASE, identities=_checks(**previous)),
+        BOUNDS,
+    )
+    assert result[0] == status, result[1]
+
+
+def test_a_known_failure_that_spreads_to_more_years_has_worsened():
+    """No-worsening by scope: the failing scopes must be a subset of the previous pair's."""
+
+    def check(scopes):
+        return _report(
+            BASE,
+            identities=[
+                {
+                    "identity_id": "sign:vat",
+                    "status": "fail",
+                    "failing_scopes": scopes,
+                }
+            ],
+        )
+
+    same = compare_metric_reports(
+        check(["2026", "2027"]), check(["2026", "2027"]), BOUNDS
+    )
+    fewer = compare_metric_reports(check(["2026"]), check(["2026", "2027"]), BOUNDS)
+    more = compare_metric_reports(
+        check(["2026", "2028"]), check(["2026", "2027"]), BOUNDS
+    )
+    assert (same[0], fewer[0], more[0]) == ("pass", "pass", "fail")
+    assert "sign:vat (fails in more scopes)" in more[1]
+    # Without scopes on both sides the failures cannot be told apart.
+    unscoped = _report(BASE, identities=[{"identity_id": "sign:vat", "status": "fail"}])
+    assert (
+        compare_metric_reports(check(["2026", "2028"]), unscoped, BOUNDS)[0] == "pass"
+    )
+
+
+@settings(max_examples=150, deadline=None)
+@given(
+    changes=st.lists(
+        st.floats(min_value=-0.5, max_value=0.5, allow_nan=False),
+        min_size=2,
+        max_size=2,
+    ),
+    shrink=st.floats(min_value=0.0, max_value=1.0),
+)
+def test_shrinking_moves_never_worsens_the_status(changes, shrink):
+    order = {"pass": 0, "needs_review": 1, "fail": 2}
+    keys = list(BASE)
+
+    def candidate(scale):
+        return _report(
+            {
+                key: BASE[key] * (1 + scale * change)
+                for key, change in zip(keys, changes)
+            }
+        )
+
+    full = compare_metric_reports(candidate(1.0), _report(BASE), BOUNDS)[0]
+    smaller = compare_metric_reports(candidate(shrink), _report(BASE), BOUNDS)[0]
+    assert order[smaller] <= order[full]
+
+
+DSA = "spending.disabled_students_allowance"
+
+
+def _cells(rows):
+    """A report from ``(metric_id, year, kind, value, suppressed, excludes)``
+    rows, with an optional seventh item giving ``exclude_signs`` (by default
+    +1 for each excluded figure)."""
+
+    report = _report({})
+    report["metrics"] = [
+        {
+            "metric_id": metric_id,
+            "year": year,
+            "kind": kind,
+            "value": value,
+            "suppressed": suppressed,
+            "excludes": list(excludes),
+            "exclude_signs": dict(signs[0])
+            if signs
+            else {excluded: 1 for excluded in excludes},
+        }
+        for metric_id, year, kind, value, suppressed, excludes, *signs in rows
+    ]
+    return report
+
+
+def _small_cell_pair(*, head=(None, True, ()), total=(5.4e11, False, (DSA,))):
+    """A suppressed head and a total rebuilt without it, for 2027."""
+
+    return _cells(
+        [
+            ("income_tax", 2027, "receipt", 3.0e11, False, ()),
+            (DSA, 2027, "spending", *head),
+            ("model.gov_spending", 2027, "model_total", *total),
+        ]
+    )
+
+
+def test_a_suppressed_figure_is_present_but_never_compared():
+    """W3's rule 1: a small cell is published as null, suppressed; not a NaN."""
+
+    status, summary, evidence = compare_metric_reports(
+        _small_cell_pair(), _small_cell_pair(), BOUNDS
+    )
+    assert status == "pass", summary
+    assert evidence["suppressed_metrics"] == [f"{DSA} 2027"]
+    assert evidence["suppressed_on_both_sides"] == 1
+    assert "2 metrics within their review bounds" in summary
+    # A null that is not marked suppressed is still a non-finite value.
+    unmarked = _small_cell_pair(head=(None, False, ()))
+    status, summary, _ = compare_metric_reports(unmarked, _small_cell_pair(), BOUNDS)
+    assert status == "fail" and f"non-finite values: {DSA} 2027" in summary
+
+
+@pytest.mark.parametrize(
+    ("candidate", "previous", "reason"),
+    [
+        (
+            _small_cell_pair(),
+            _small_cell_pair(head=(2.0e6, False, ()), total=(5.4e11, False, ())),
+            "suppressed in the candidate report",
+        ),
+        (
+            _small_cell_pair(head=(2.0e6, False, ()), total=(5.4e11, False, ())),
+            _small_cell_pair(),
+            "suppressed in the previous report",
+        ),
+    ],
+    ids=["newly suppressed", "no longer suppressed"],
+)
+def test_a_figure_suppressed_on_one_side_is_withheld(candidate, previous, reason):
+    """Its change would come from fewer than 10 records: never computed."""
+
+    status, summary, evidence = compare_metric_reports(candidate, previous, BOUNDS)
+    assert status == "needs_review", summary
+    assert "withheld" in summary
+    withheld = {
+        (row["metric_id"], row["reason"]) for row in evidence["withheld_changes"]
+    }
+    assert withheld == {(DSA, reason)}
+    # The total is lined up: the side that publishes the head takes it out.
+    assert evidence["lined_up_totals"] == ["model.gov_spending 2027"]
+    assert evidence["moves_beyond_bounds"] == []
+
+
+def _one_sided(kind, sign, *, rest_c, rest_p, head_p=2.0e10, metric="total"):
+    """A total over a head suppressed in the candidate and published in the
+    previous report, which it holds there with ``sign``. Values are synthetic."""
+
+    candidate = _cells(
+        [
+            (DSA, 2027, "spending", None, True, ()),
+            (metric, 2027, kind, rest_c, False, (DSA,), {DSA: sign}),
+        ]
+    )
+    previous = _cells(
+        [
+            (DSA, 2027, "spending", head_p, False, ()),
+            (metric, 2027, kind, rest_p + sign * head_p, False, (), {}),
+        ]
+    )
+    return candidate, previous
+
+
+@pytest.mark.parametrize("sign", [1, -1])
+@pytest.mark.parametrize(
+    ("kind", "metric"),
+    [
+        ("model_total", "model.gov_spending"),
+        ("balance", "balance.balance"),
+        ("household_total", "household.household_net_income"),
+    ],
+)
+def test_additive_totals_are_lined_up_from_published_figures(kind, metric, sign):
+    """As the tool's compare._aligned: the previous side takes out the head
+    only the candidate excludes, at its own published value and the
+    candidate's sign. The head (£20bn, synthetic) is four times the bound,
+    so comparing the totals as published would flag it."""
+
+    same, previous = _one_sided(kind, sign, rest_c=5.0e11, rest_p=5.0e11, metric=metric)
+    status, _, evidence = compare_metric_reports(same, previous, BOUNDS)
+    assert status == "needs_review"  # the head itself is withheld
+    assert evidence["lined_up_totals"] == [f"{metric} 2027"]
+    assert evidence["moves_beyond_bounds"] == []
+    assert [row["metric_id"] for row in evidence["withheld_changes"]] == [DSA]
+    moved, previous = _one_sided(
+        kind, sign, rest_c=5.3e11, rest_p=5.0e11, metric=metric
+    )
+    (move,) = compare_metric_reports(moved, previous, BOUNDS)[2]["moves_beyond_bounds"]
+    assert move["metric_id"] == metric and move["lined_up_without"] == [DSA]
+    assert move["change"] == pytest.approx(3.0e10)
+    # The published levels are recorded, never a lined-up one.
+    assert move["previous"] == 5.0e11 + sign * 2.0e10
+    assert move["candidate"] == 5.3e11
+    # Lined up the other way round: the candidate publishes the head.
+    status, _, evidence = compare_metric_reports(previous, moved, BOUNDS)
+    assert evidence["lined_up_totals"] == [f"{metric} 2027"]
+    assert evidence["moves_beyond_bounds"][0]["change"] == pytest.approx(-3.0e10)
+
+
+def test_the_relative_bound_is_taken_on_the_previous_published_total():
+    """W3: the tool flags with threshold.exceeded(b, b + delta), b the base's
+    published level. Published previous £5.0bn (with the head £2.0bn),
+    lined-up previous £3.0bn: 2% is £100m on the first, £60m on the second."""
+
+    bounds = {"default": {"relative": 0.02, "absolute": 0.0}}
+
+    def change(by):
+        return _one_sided(
+            "model_total", 1, rest_c=3.0e9 + by, rest_p=3.0e9, head_p=2.0e9
+        )
+
+    within, previous = change(8.0e7)
+    assert (
+        compare_metric_reports(within, previous, bounds)[2]["moves_beyond_bounds"] == []
+    )
+    beyond, previous = change(1.2e8)
+    (move,) = compare_metric_reports(beyond, previous, bounds)[2]["moves_beyond_bounds"]
+    assert move["threshold"] == pytest.approx(1.0e8)
+
+
+@pytest.mark.parametrize(
+    ("kind", "metric"),
+    [
+        ("decile", "deciles.mean_income.3"),
+        ("household_total", "household.hbai_household_net_income"),
+        ("caseload", "caseloads.child_benefit"),
+        ("poverty", "poverty.relative_bhc"),
+    ],
+)
+def test_other_totals_with_different_exclusions_are_withheld(kind, metric):
+    """Decile means and the HBAI total (reset at zero) are not sums of
+    heads, so different exclusions cannot be lined up: withheld."""
+
+    candidate, previous = _one_sided(
+        kind, 1, rest_c=5.0e11, rest_p=5.0e11, metric=metric
+    )
+    status, summary, evidence = compare_metric_reports(candidate, previous, BOUNDS)
+    assert status == "needs_review", summary
+    assert {
+        "metric_id": metric,
+        "year": 2027,
+        "reason": "the reports leave out different suppressed figures",
+        "candidate_excludes": [DSA],
+        "previous_excludes": [],
+    } in evidence["withheld_changes"]
+    assert evidence["lined_up_totals"] == evidence["moves_beyond_bounds"] == []
+
+
+def test_totals_that_cannot_be_lined_up_are_withheld():
+    other = "spending.other_head"
+    # The head the previous side would take out is not in its report.
+    candidate, previous = _one_sided("model_total", 1, rest_c=5.0e11, rest_p=5.0e11)
+    previous["metrics"] = [
+        row for row in previous["metrics"] if row["metric_id"] != DSA
+    ]
+    evidence = compare_metric_reports(candidate, previous, BOUNDS)[2]
+    assert evidence["withheld_changes"][-1]["reason"] == (
+        "the reports leave out different suppressed figures, and one needed "
+        "to line them up is not published"
+    )
+    assert evidence["lined_up_totals"] == []
+    # A figure both sides exclude enters with different signs (the partition
+    # changed): the two totals are not the same sum.
+    candidate = _cells(
+        [
+            (DSA, 2027, "spending", None, True, ()),
+            (other, 2027, "spending", None, True, ()),
+            ("total", 2027, "balance", 5.0e11, False, (DSA, other), {DSA: 1, other: 1}),
+        ]
+    )
+    previous = _cells(
+        [
+            (DSA, 2027, "spending", None, True, ()),
+            (other, 2027, "spending", 1.0e9, False, ()),
+            ("total", 2027, "balance", 5.0e11, False, (DSA,), {DSA: -1}),
+        ]
+    )
+    evidence = compare_metric_reports(candidate, previous, BOUNDS)[2]
+    assert evidence["withheld_changes"][-1]["reason"] == (
+        "a figure both reports leave out enters their totals with different signs"
+    )
+    # Signs that do not match the exclusions fail the report, and nothing
+    # is lined up from them.
+    candidate, previous = _one_sided("model_total", 1, rest_c=5.0e11, rest_p=5.0e11)
+    candidate["metrics"][1]["exclude_signs"] = {}
+    status, summary, evidence = compare_metric_reports(candidate, previous, BOUNDS)
+    assert status == "fail" and "exclude_signs that are not +1 or -1" in summary
+    assert evidence["values_withheld"] is True
+    assert evidence["withheld_changes"] == evidence["moves_beyond_bounds"] == []
+
+
+def test_totals_at_equal_exclusions_compare_numerically():
+    small = _small_cell_pair(total=(5.4e11 + 1.0e9, False, (DSA,)))
+    assert compare_metric_reports(small, _small_cell_pair(), BOUNDS)[0] == "pass"
+    large = _small_cell_pair(total=(6.0e11, False, (DSA,)))
+    status, _, evidence = compare_metric_reports(large, _small_cell_pair(), BOUNDS)
+    assert status == "needs_review"
+    assert [move["metric_id"] for move in evidence["moves_beyond_bounds"]] == [
+        "model.gov_spending"
+    ]
+    assert evidence["withheld_changes"] == []
+
+
+@pytest.mark.parametrize(
+    "kind", ["decile", "household_total", "model_total", "balance"]
+)
+def test_the_same_figure_excluded_with_opposite_signs_is_withheld(kind):
+    """supp-r2 P1: equal excluded ids are not equal exclusions when a sign
+    differs (the tool compares the signed maps), so the change is withheld,
+    never computed, in every section and both review steps."""
+
+    def side(sign, value):
+        return _cells(
+            [
+                (DSA, 2027, "spending", None, True, ()),
+                ("total", 2027, kind, value, False, (DSA,), {DSA: sign}),
+            ]
+        )
+
+    candidate, previous = side(1, 9.0e11), side(-1, 5.0e11)
+    status, _, evidence = compare_metric_reports(candidate, previous, BOUNDS)
+    assert status == "needs_review"
+    assert evidence["moves_beyond_bounds"] == evidence["lined_up_totals"] == []
+    (row,) = evidence["withheld_changes"]
+    assert row["metric_id"] == "total"
+    # One figure with two signs says so in every section (supp-r4 N01).
+    assert row["reason"] == (
+        "a figure both reports leave out enters their totals with different signs"
+    )
+    result, steps = metrics_review(
+        candidate_report=candidate,
+        previous_report=previous,
+        bounds=BOUNDS,
+        cross_report=candidate,
+    )
+    assert steps["model_only"]["moves_beyond_bounds"] == []
+    assert steps["model_only"]["withheld_changes"][0]["metric_id"] == "total"
+
+
+def test_figures_both_sides_exclude_are_skipped_when_lining_up():
+    """supp-r2 M03: the candidate excludes A and B, the previous report A
+    only; B (published there) is taken out of the previous total, A of
+    neither (it is suppressed on both sides). Values synthetic."""
+
+    other = "spending.other_head"
+    candidate = _cells(
+        [
+            (DSA, 2027, "spending", None, True, ()),
+            (other, 2027, "spending", None, True, ()),
+            (
+                "total",
+                2027,
+                "model_total",
+                4.0e11,
+                False,
+                (DSA, other),
+                {DSA: 1, other: -1},
+            ),
+        ]
+    )
+    previous = _cells(
+        [
+            (DSA, 2027, "spending", None, True, ()),
+            (other, 2027, "spending", 3.0e10, False, ()),
+            ("total", 2027, "model_total", 4.0e11 - 3.0e10, False, (DSA,), {DSA: 1}),
+        ]
+    )
+    evidence = compare_metric_reports(candidate, previous, BOUNDS)[2]
+    assert evidence["lined_up_totals"] == ["total 2027"]
+    assert evidence["moves_beyond_bounds"] == []
+
+
+def test_a_caseload_can_be_suppressed_beside_its_published_head():
+    """supp-r2 P2: a published head's caseload counts only its positive
+    records, so a head with negative values can be published while its
+    caseload is suppressed. Valid, and published to the ledger."""
+
+    from policyengine.provenance.uk_candidate import publishable_metrics_report
+
+    report = _small_cell_pair(head=(2.0e6, False, ()), total=(5.4e11, False, ()))
+    report["metrics"].append(
+        {
+            "metric_id": "caseloads.disabled_students_allowance",
+            "year": 2027,
+            "kind": "caseload",
+            "value": None,
+            "suppressed": True,
+            "excludes": [],
+            "exclude_signs": {},
+        }
+    )
+    status, summary, _ = compare_metric_reports(report, report, BOUNDS)
+    assert status == "pass", summary
+    assert publishable_metrics_report(report) == report
+
+
+def test_no_value_from_an_unsafe_report_enters_the_evidence():
+    """supp-r2 P1: a report that breaks the small-cell format fails, and
+    neither the comparison's moves nor the responsiveness table carries any
+    of its values (the ledger publishes both). Values synthetic."""
+
+    marker = 123456789.25
+    legacy = _report({("income_tax", 2027, "receipt"): marker})
+    for row in legacy["metrics"]:
+        for field in ("suppressed", "excludes", "exclude_signs"):
+            row.pop(field)
+    status, _, evidence = compare_metric_reports(
+        legacy, _report({("income_tax", 2027, "receipt"): 3.0e11}), BOUNDS
+    )
+    assert status == "fail" and evidence["values_withheld"] is True
+    assert str(marker) not in json.dumps(evidence)
+    result, steps = metrics_review(
+        candidate_report=legacy,
+        previous_report=_report({("income_tax", 2027, "receipt"): 3.0e11}),
+        bounds=BOUNDS,
+        cross_report=legacy,
+    )
+    assert str(marker) not in json.dumps(steps)
+
+    report = _responsive_report()
+    for row in report["metrics"]:
+        row.pop("exclude_signs")
+    result, evidence = reform_responsiveness_gate(report)
+    assert result.status == "error"
+    assert "rows without the small-cell fields" in result.summary
+    for reform in evidence["reforms"]:
+        assert set(reform["target_changes"].values()) == {None}
+        assert reform["detail"] is None
+
+
+def test_a_report_the_tool_could_not_finish_is_still_checked_before_publication():
+    """supp-r2 M28: an unfinished report with metrics in it is held to the
+    same format before the ledger publishes it."""
+
+    from policyengine.provenance.uk_candidate import publishable_metrics_report
+
+    report = _small_cell_pair()
+    report["status"] = "error"
+    report["metrics"][1]["value"] = 2.0e6
+    assert "withheld" in publishable_metrics_report(report)
+    unfinished = {"status": "error", "error": "Traceback ..."}
+    assert publishable_metrics_report(unfinished) == unfinished
+
+
+@pytest.mark.parametrize(
+    ("mutate", "message"),
+    [
+        (
+            lambda rows: rows[1].update(value=2.0e6),
+            f"suppressed figures with a value: {DSA} 2027",
+        ),
+        (
+            lambda rows: rows[1].update(suppressed="yes"),
+            "a suppressed flag that is not true or false",
+        ),
+        (
+            lambda rows: rows[2].update(excludes=DSA),
+            "excludes that are not a list of metric ids",
+        ),
+        (
+            lambda rows: rows[2].update(excludes=[DSA, 7]),
+            "excludes that are not a list of metric ids",
+        ),
+        (
+            lambda rows: rows[2].update(excludes=[DSA, "income_tax"]),
+            "totals excluding a figure it does not suppress: "
+            "model.gov_spending 2027 excludes income_tax",
+        ),
+        (
+            lambda rows: rows[2].update(year=2028),
+            "totals excluding a figure it does not suppress",
+        ),
+        (
+            lambda rows: rows[0].pop("exclude_signs"),
+            "rows without the small-cell fields",
+        ),
+        (
+            lambda rows: [row.pop("suppressed") for row in rows],
+            "rows without the small-cell fields",
+        ),
+        (
+            lambda rows: rows[0].update(year="2027"),
+            "metric rows whose year is not an integer",
+        ),
+        (
+            lambda rows: rows[2].update(exclude_signs={DSA: 2}),
+            "exclude_signs that are not +1 or -1 for exactly its excludes",
+        ),
+        (
+            lambda rows: rows[2].update(exclude_signs={DSA: True}),
+            "exclude_signs that are not +1 or -1",
+        ),
+        (
+            lambda rows: rows[2].update(exclude_signs={DSA: 1, "income_tax": -1}),
+            "exclude_signs that are not +1 or -1",
+        ),
+        (
+            lambda rows: rows[2].update(exclude_signs=[DSA]),
+            "exclude_signs that are not +1 or -1",
+        ),
+        (
+            # W3's measure_year suppresses the caseload with its head, even
+            # when the caseload is zero.
+            lambda rows: rows.append(
+                {
+                    "metric_id": "caseloads.disabled_students_allowance",
+                    "year": 2027,
+                    "kind": "caseload",
+                    "value": 0.0,
+                    **PUBLISHED,
+                }
+            ),
+            "caseloads published beside their suppressed spending head: "
+            "caseloads.disabled_students_allowance 2027",
+        ),
+        (
+            lambda rows: rows[1].update(year=True),
+            "metric rows whose year is not an integer",
+        ),
+        (
+            lambda rows: rows[2].update(exclude_signs={DSA: 0}),
+            "exclude_signs that are not +1 or -1",
+        ),
+        (
+            lambda rows: rows.append(dict(rows[0])),
+            "duplicate metrics: income_tax 2027",
+        ),
+    ],
+    ids=[
+        "suppressed with a value",
+        "flag not boolean",
+        "excludes a string",
+        "excludes a number",
+        "excludes an unsuppressed figure",
+        "excludes a figure suppressed in another year",
+        "no exclude_signs",
+        "no suppressed flags (a tool from before suppression)",
+        "year as text",
+        "sign of 2",
+        "sign True",
+        "signs for a figure not excluded",
+        "signs not a map",
+        "zero caseload published beside a suppressed head",
+        "year True",
+        "sign of 0",
+        "a figure twice",
+    ],
+)
+@pytest.mark.parametrize("side", ["candidate", "previous"])
+def test_a_report_that_breaks_the_small_cell_format_fails(mutate, message, side):
+    broken = _small_cell_pair()
+    mutate(broken["metrics"])
+    pair = (broken, _small_cell_pair())
+    if side == "previous":
+        pair = pair[::-1]
+    status, summary, _ = compare_metric_reports(*pair, BOUNDS)
+    assert status == "fail", summary
+    assert f"the {side} report has" in summary and message in summary
+
+
+def test_numpy_scalars_in_evidence_become_the_numbers_they_hold():
+    """supp-r3 Low 2 and supp-r4 J03 and nit: NumPy bool, integer and float
+    scalars are unwrapped before the non-finite check (a float32 NaN is
+    "NaN", never an error); other NumPy values, such as a 0-d array or a
+    datetime64, are left for strict JSON to refuse."""
+
+    import numpy as np
+
+    safe = gates_module._json_safe
+    assert safe(np.int64(4)) == 4 and type(safe(np.int64(4))) is int
+    assert safe(np.bool_(True)) is True
+    assert safe(np.float32("nan")) == "NaN"
+    assert safe(np.float64("-inf")) == "-Infinity"
+    assert gates_module.evidence_sha256({"x": np.float32("nan")})
+    array = np.array(4)
+    assert safe(array) is array
+    stamp = np.datetime64("2026-10-16T00:00:00", "ns")
+    assert safe(stamp) is stamp
+
+
+def test_a_boolean_metric_value_is_not_a_number():
+    report = _report(BASE)
+    report["metrics"][0]["value"] = True
+    status, summary, _ = compare_metric_reports(report, _report(BASE), BOUNDS)
+    assert status == "fail" and "non-finite values" in summary
+
+
+@pytest.mark.parametrize("side", ["candidate", "previous"])
+def test_a_repeated_check_cannot_decide_the_comparison(side):
+    """A second copy of a check must not turn a regression into a carried
+    failure (supp-r1 review): a repeated id fails the report."""
+
+    regression = (
+        _report(BASE, identities=[{"identity_id": "sign:x", "status": "fail"}]),
+        _report(BASE, identities=[{"identity_id": "sign:x", "status": "pass"}]),
+    )
+    assert compare_metric_reports(*regression, BOUNDS)[0] == "fail"
+    candidate, previous = regression
+    repeated = previous if side == "previous" else candidate
+    repeated["identities"].append(
+        {"identity_id": "sign:x", "status": "fail" if side == "previous" else "pass"}
+    )
+    status, summary, _ = compare_metric_reports(candidate, previous, BOUNDS)
+    assert status == "fail" and f"the {side} report repeats checks: sign:x" in summary
+
+
+@pytest.mark.parametrize("status", ["INVALID", None, "PASS"])
+def test_an_unknown_status_in_the_previous_report_is_not_a_pass(status):
+    """It decides new warnings and regressions, so it fails like the
+    candidate's would (supp-r1 review)."""
+
+    candidate = _report(BASE, identities=[{"identity_id": "inert:x", "status": "warn"}])
+    previous = _report(BASE, identities=[{"identity_id": "inert:x", "status": status}])
+    result = compare_metric_reports(candidate, previous, BOUNDS)
+    assert result[0] == "fail"
+    assert "the previous report has checks with an unknown status: inert:x" in result[1]
+
+
+@pytest.mark.parametrize(
+    ("check", "previous", "status", "newly", "hard"),
+    [
+        # W3's rule 4: skipped means not run.
+        ("sign:dsa", "skipped", "pass", False, False),
+        ("sign:dsa", None, "pass", False, False),
+        ("sign:dsa", "pass", "needs_review", True, False),
+        ("sign:dsa", "warn", "needs_review", True, False),
+        # A known failure that is no longer run is not a fix.
+        ("sign:dsa", "fail", "needs_review", True, False),
+        # A hard check that was not run is never a pass.
+        ("closure", "skipped", "needs_review", False, True),
+        ("finite:metrics", None, "needs_review", False, True),
+        ("responsive:x", "pass", "needs_review", True, True),
+    ],
+)
+def test_a_skipped_check_is_not_run(check, previous, status, newly, hard):
+    before = [] if previous is None else [{"identity_id": check, "status": previous}]
+    result = compare_metric_reports(
+        _report(BASE, identities=[{"identity_id": check, "status": "skipped"}]),
+        _report(BASE, identities=before),
+        BOUNDS,
+    )
+    assert result[0] == status, result[1]
+    evidence = result[2]
+    assert evidence["skipped_checks"] == [check]
+    assert evidence["newly_skipped_checks"] == ([check] if newly else [])
+    assert evidence["hard_checks_not_run"] == ([check] if hard else [])
+
+
+_CELL = st.tuples(
+    st.sampled_from(["published", "suppressed"]),
+    st.sampled_from([(), (DSA,), (DSA, "other_head")]),
+    st.floats(min_value=-0.5, max_value=0.5, allow_nan=False),
+)
+
+
+@settings(max_examples=300, deadline=None)
+@given(cells=st.lists(st.tuples(_CELL, _CELL), min_size=1, max_size=4))
+def test_the_small_cell_rule_against_a_reference(cells):
+    """Differential: the gate's status for small cells matches the rule
+    stated independently, and no withheld change carries a value."""
+
+    def side(choices):
+        rows = [
+            (DSA, 2027, "spending", None, True, ()),
+            ("other_head", 2027, "spending", None, True, ()),
+        ]
+        for i, (state, excludes, change) in enumerate(choices):
+            value = None if state == "suppressed" else 1.0e11 * (1 + change)
+            rows.append(
+                (f"m{i}", 2027, "model_total", value, state == "suppressed", excludes)
+            )
+        return _cells(rows)
+
+    candidate = side([pair[0] for pair in cells])
+    previous = side([pair[1] for pair in cells])
+    expected = "pass"
+    for (state_c, ex_c, ch_c), (state_p, ex_p, ch_p) in cells:
+        if state_c == state_p == "suppressed":
+            continue
+        if "suppressed" in (state_c, state_p) or tuple(sorted(ex_c)) != tuple(
+            sorted(ex_p)
+        ):
+            expected = "needs_review"
+            continue
+        new, old = 1.0e11 * (1 + ch_c), 1.0e11 * (1 + ch_p)
+        if abs(new - old) > max(0.02 * abs(old), 5e8):
+            expected = "needs_review"
+    status, summary, evidence = compare_metric_reports(candidate, previous, BOUNDS)
+    assert status == expected, summary
+    for row in evidence["withheld_changes"]:
+        assert set(row) <= {
+            "metric_id",
+            "year",
+            "reason",
+            "candidate_excludes",
+            "previous_excludes",
+        }
+
+
+_SIDE = st.tuples(
+    st.sampled_from(["published", "suppressed"]),
+    # Heads can be negative (supp-r2 M06): their published value is taken
+    # out with the sign, whatever its own sign.
+    st.one_of(
+        st.floats(min_value=1.0e8, max_value=5.0e10),
+        st.floats(min_value=-5.0e10, max_value=-1.0e8),
+    ),
+    st.floats(min_value=-0.5, max_value=0.5, allow_nan=False),
+    # Each side's sign for the head; they differ only if the partition
+    # changed (supp-r2: opposite signs for one figure were never generated).
+    st.sampled_from([1, -1]),
+)
+
+
+@settings(max_examples=500, deadline=None)
+@given(
+    candidate_side=_SIDE,
+    previous_side=_SIDE,
+    kind=st.sampled_from(["model_total", "balance", "decile"]),
+)
+def test_lining_up_against_a_reference(candidate_side, previous_side, kind):
+    """Differential against the tool's rule, stated independently: a head
+    published or suppressed on each side, a total that holds it with that
+    side's sign where it is published and excludes it, with that sign, where
+    it is not."""
+
+    def side(state, head, change, sign):
+        rest = 4.0e11 * (1 + change)
+        if state == "suppressed":
+            rows = [
+                (DSA, 2027, "spending", None, True, ()),
+                ("total", 2027, kind, rest, False, (DSA,), {DSA: sign}),
+            ]
+        else:
+            rows = [
+                (DSA, 2027, "spending", head, False, ()),
+                ("total", 2027, kind, rest + sign * head, False, (), {}),
+            ]
+        return _cells(rows), rows
+
+    candidate, c_rows = side(*candidate_side)
+    previous, p_rows = side(*previous_side)
+    (c_state, c_head, _, c_sign), (p_state, p_head, _, p_sign) = (
+        candidate_side,
+        previous_side,
+    )
+    c_total, p_total = c_rows[1][3], p_rows[1][3]
+    review, lined_up, change = False, False, None
+    if c_state == p_state == "published":
+        head_threshold = max(0.02 * abs(p_head), 5e8)
+        review |= abs(c_head - p_head) > head_threshold
+        change = c_total - p_total
+    elif c_state == p_state == "suppressed":
+        if c_sign == p_sign:
+            change = c_total - p_total
+        else:
+            review = True  # one figure, two signs: withheld in every section
+    else:
+        review = True  # the head is withheld
+        if kind in ("model_total", "balance"):
+            lined_up = True
+            # The side that publishes the head takes it out with the sign
+            # the excluding side records, exactly as the gate computes it.
+            if c_state == "published":
+                change = (c_total - p_sign * c_head) - p_total
+            else:
+                change = c_total - (p_total - c_sign * p_head)
+    moved = change is not None and abs(change) > max(0.02 * abs(p_total), 5e8)
+    review |= moved
+    status, summary, evidence = compare_metric_reports(candidate, previous, BOUNDS)
+    assert status == ("needs_review" if review else "pass"), summary
+    assert evidence["lined_up_totals"] == (["total 2027"] if lined_up else [])
+    total_moves = [
+        m for m in evidence["moves_beyond_bounds"] if m["metric_id"] == "total"
+    ]
+    assert bool(total_moves) == moved
+    if moved:
+        assert total_moves[0]["change"] == change
+    for row in evidence["withheld_changes"]:
+        assert set(row) <= {
+            "metric_id",
+            "year",
+            "reason",
+            "candidate_excludes",
+            "previous_excludes",
+        }
+
+
+@settings(max_examples=200, deadline=None)
+@given(
+    rows=st.lists(
+        st.tuples(
+            st.sampled_from(["income_tax", "vat", DSA]),
+            st.sampled_from([2026, 2027]),
+            st.one_of(st.none(), st.floats(allow_nan=True, allow_infinity=True)),
+            st.sampled_from([True, False]),
+            st.sampled_from([(), (DSA,)]),
+        ),
+        min_size=1,
+        max_size=6,
+    )
+)
+def test_a_suppressed_cell_never_hides_a_hard_failure(rows):
+    """Whatever the cells, a failed hard check still fails the comparison."""
+
+    report = _cells([(m, y, "receipt", v, s, e) for m, y, v, s, e in rows])
+    report["identities"] = [{"identity_id": "closure", "status": "fail"}]
+    assert compare_metric_reports(report, report, BOUNDS)[0] == "fail"
+
+
+def test_metrics_gate_splits_model_and_data_steps():
+    cross = {**BASE, ("income_tax", 2027, "receipt"): 3.2e11}
+    result, evidence = metrics_review(
+        candidate_report=_report(cross),
+        previous_report=_report(BASE),
+        bounds=BOUNDS,
+        cross_report=_report(cross),
+    )
+    assert result.details == {"model_only": "needs_review", "data_only": "pass"}
+    assert result.status == "needs_review"
+    assert (
+        metrics_review(candidate_report=None, previous_report=None, bounds=BOUNDS)[
+            0
+        ].status
+        == "unavailable"
+    )
+
+
+# --- Orchestration -------------------------------------------------------------------
+
+
+@settings(max_examples=50, deadline=None)
+@given(
+    outcomes=st.lists(
+        st.sampled_from(["pass", "fail", "raise"]), min_size=1, max_size=9
+    )
+)
+def test_run_gates_returns_one_result_per_gate(outcomes):
+    def make(index, outcome):
+        def function():
+            if outcome == "raise":
+                raise RuntimeError(f"gate {index} crashed")
+            return gates_module._result(f"wrong-id-{index}", outcome, "x", {})
+
+        return (f"gate_{index}", function)
+
+    results, evidence = run_gates([make(i, o) for i, o in enumerate(outcomes)])
+    assert [r.gate_id for r in results] == [f"gate_{i}" for i in range(len(outcomes))]
+    for result, outcome in zip(results, outcomes):
+        assert result.status == ("error" if outcome == "raise" else outcome)
+    assert set(evidence) == {f"gate_{i}" for i in range(len(outcomes))}
+
+
+# --- Previous-pair runners -----------------------------------------------------------
+
+
+def _write_previous_receipt(root, pair, evidence, *, attempt="a"):
+    from policyengine.provenance.qualification_ledger import write_json
+
+    receipt = make_receipt(pair)
+    receipt = receipt.model_copy(
+        update={
+            "gates": [
+                gate.model_copy(
+                    update={
+                        "evidence_sha256": gates_module.evidence_sha256(
+                            evidence[gate.gate_id]
+                        )
+                    }
+                )
+                if gate.gate_id in evidence
+                else gate
+                for gate in receipt.gates
+            ]
+        }
+    )
+    pair_dir = root / "uk" / pair.pair_id
+    write_json(pair_dir / "qualification_receipt.json", receipt.canonical_payload())
+    attempt_dir = pair_dir / "attempts" / attempt
+    write_json(attempt_dir / "qualification_receipt.json", receipt.canonical_payload())
+    write_json(attempt_dir / "harness.json", receipt.harness.canonical_payload())
+    for gate_id, value in evidence.items():
+        write_json(attempt_dir / "gates" / f"{gate_id}.json", value)
+    return receipt, attempt_dir
+
+
+def _previous_identity(pair):
+    return {
+        "bundle_version": HARNESS.policyengine_version,
+        "model_version": pair.model.version,
+        "core_version": pair.core.version,
+        "data_release_version": pair.data.release_version,
+        "dataset_sha256": pair.data.dataset_sha256,
+        "qualification_pair_id": pair.pair_id,
+        "model_wheel_sha256": pair.model.wheel_sha256,
+        "core_wheel_sha256": pair.core.wheel_sha256,
+    }
+
+
+def test_previous_evidence_matches_the_exact_certified_core_and_wheels(tmp_path):
+    wrong = make_pair(core="3.32.0")
+    certified = make_pair(core="3.33.0")
+    wrong_evidence = {
+        "supported_years": {"supported_years": [2025]},
+        "budget_inputs": {"missing_inputs": ["age"], "unusable_critical": []},
+    }
+    expected = {
+        "supported_years": {"supported_years": [2025, 2026]},
+        "budget_inputs": {"missing_inputs": [], "unusable_critical": []},
+    }
+    _write_previous_receipt(tmp_path, wrong, wrong_evidence)
+    _write_previous_receipt(tmp_path, certified, expected)
+    assert (
+        gates_module._previous_gate_evidence(tmp_path, _previous_identity(certified))
+        == expected
+    )
+    for wheel in ("core_wheel_sha256", "model_wheel_sha256"):
+        altered = {**_previous_identity(certified), wheel: "0" * 64}
+        assert gates_module._previous_gate_evidence(tmp_path, altered) == {}
+
+
+def test_previous_evidence_belongs_to_the_receipts_attempt(tmp_path):
+    from policyengine.provenance.qualification_ledger import write_json
+
+    pair = make_pair()
+    expected = {
+        "supported_years": {"supported_years": [2025, 2026]},
+        "budget_inputs": {"missing_inputs": [], "unusable_critical": []},
+    }
+    receipt, attempt = _write_previous_receipt(tmp_path, pair, expected)
+    unrelated = attempt.parent / "z"
+    other_receipt = receipt.model_copy(
+        update={"harness": HARNESS.model_copy(update={"git_commit": "1" * 40})}
+    )
+    write_json(
+        unrelated / "qualification_receipt.json", other_receipt.canonical_payload()
+    )
+    for name in expected:
+        write_json(unrelated / "gates" / f"{name}.json", {"untrusted": True})
+    assert (
+        gates_module._previous_gate_evidence(tmp_path, _previous_identity(pair))
+        == expected
+    )
+
+
+@pytest.mark.parametrize(
+    "corruption", ["digest", "shape", "receipt", "missing identity"]
+)
+def test_unverified_previous_evidence_is_recomputed(tmp_path, corruption):
+    from policyengine.provenance.qualification_ledger import write_json
+
+    pair = make_pair()
+    expected = {
+        "supported_years": {"supported_years": [2025, 2026]},
+        "budget_inputs": {"missing_inputs": [], "unusable_critical": []},
+    }
+    receipt, attempt = _write_previous_receipt(tmp_path, pair, expected)
+    previous = _previous_identity(pair)
+    if corruption == "digest":
+        write_json(
+            attempt / "gates" / "supported_years.json", {"supported_years": [2025]}
+        )
+    elif corruption == "shape":
+        invalid = {**expected, "supported_years": {"supported_years": [True]}}
+        _write_previous_receipt(tmp_path, pair, invalid)
+    elif corruption == "receipt":
+        payload = receipt.canonical_payload() | {"verdict": "needs_review"}
+        write_json(attempt.parent.parent / "qualification_receipt.json", payload)
+    else:
+        previous.pop("qualification_pair_id")
+        previous.pop("core_wheel_sha256")
+    assert gates_module._previous_gate_evidence(tmp_path, previous) == {}
+
+
+@pytest.mark.parametrize("corrupt_baseline", [False, True])
+def test_orchestration_hashes_every_finalized_gate_evidence(
+    tmp_path, monkeypatch, corrupt_baseline
+):
+    from policyengine.provenance import dataset_materialization
+    from policyengine.provenance.qualification_ledger import write_json
+
+    pair = make_pair()
+    config = json.loads(
+        (
+            Path(__file__).parents[1] / "src/policyengine/data/qualification/uk.json"
+        ).read_text()
+    )
+    monkeypatch.setattr(
+        dataset_materialization,
+        "materialize_dataset",
+        lambda *a, **k: SimpleNamespace(path=tmp_path / "dataset.h5"),
+    )
+    monkeypatch.setitem(
+        sys.modules,
+        "policyengine_uk.system",
+        SimpleNamespace(system=SimpleNamespace(variables={})),
+    )
+    baseline = {
+        "budget_inputs": {"missing_inputs": [], "unusable_critical": []},
+        "supported_years": {"supported_years": [2025]},
+    }
+    _, attempt = _write_previous_receipt(tmp_path, pair, baseline)
+    if corrupt_baseline:
+        write_json(
+            attempt / "gates" / "supported_years.json", {"supported_years": [2024]}
+        )
+    snapshots = []
+
+    def snapshot(*args, **kwargs):
+        snapshots.append(args[0])
+        return {
+            "missing_inputs": [],
+            "unusable_critical": [],
+            "supported_years": [2025],
+        }
+
+    monkeypatch.setattr(gates_module, "run_inputs_snapshot", snapshot)
+    monkeypatch.setattr(gates_module, "vendor_metrics_tool", lambda *a, **k: tmp_path)
+    monkeypatch.setattr(gates_module, "load_standard_reforms", lambda *a: [])
+    monkeypatch.setattr(gates_module, "previous_pair_python", lambda *a: "python")
+    monkeypatch.setattr(
+        gates_module, "previous_pair_dataset", lambda *a: tmp_path / "dataset.h5"
+    )
+    monkeypatch.setattr(
+        gates_module,
+        "run_metrics_report",
+        lambda *a, **k: {"partition_source": "test", "dataset_path": "dataset.h5"},
+    )
+    monkeypatch.setattr(
+        gates_module, "_download", lambda *a, **k: tmp_path / "wheel.whl"
+    )
+    for name in (
+        "installed_versions",
+        "wheel_hashes",
+        "core_compatibility",
+        "dataset_bytes",
+        "supported_years",
+        "budget_inputs",
+        "reform_contract",
+        "metrics_review",
+        "reform_responsiveness",
+    ):
+        monkeypatch.setattr(
+            gates_module,
+            name,
+            lambda *a, _name=name, **k: gates_module._result(
+                _name, "pass", "test", {"before_metadata": _name}
+            ),
+        )
+    monkeypatch.setattr(
+        gates_module,
+        "data_access",
+        lambda *a, **k: (*gates_module._result("data_access", "pass", "test", {}), {}),
+    )
+    resolved = SimpleNamespace(
+        pair=pair,
+        model_wheel=SimpleNamespace(
+            url="model", filename="model.whl", sha256=pair.model.wheel_sha256
+        ),
+        core_wheel=SimpleNamespace(
+            url="core", filename="core.whl", sha256=pair.core.wheel_sha256
+        ),
+        model_requires_core=">=3.0",
+    )
+    results, evidence = gates_module.run_candidate_gates(
+        resolved,
+        HARNESS,
+        config=config,
+        work_dir=tmp_path,
+        previous_certificate=_previous_identity(pair),
+        ledger_root=tmp_path,
+    )
+    mismatches = [
+        result.gate_id
+        for result in results
+        if result.evidence_sha256
+        != gates_module.evidence_sha256(evidence[result.gate_id])
+    ]
+    assert mismatches == []
+    assert snapshots == (["python"] if corrupt_baseline else [])
+    assert evidence["budget_inputs"]["previous_source"] == (
+        f"computed with policyengine {HARNESS.policyengine_version}"
+        if corrupt_baseline
+        else "ledger"
+    )
+
+
+@settings(max_examples=15, deadline=None)
+@given(
+    present=st.sets(st.sampled_from(["age", "employment_income", "rent"]), min_size=1),
+    critical=st.lists(
+        st.sampled_from(["age", "rent", "would_claim_uc", "missing_x"]),
+        unique=True,
+        max_size=3,
+    ),
+    projected=st.lists(st.integers(2022, 2032), min_size=1, max_size=6, unique=True),
+)
+def test_inputs_runner_agrees_with_the_gates(
+    tmp_path_factory, present, critical, projected
+):
+    """Differential: the previous-pair runner and the in-process gates agree."""
+
+    import sys
+
+    from policyengine.provenance.uk_candidate import (
+        dataset_columns,
+        run_inputs_snapshot,
+    )
+
+    directory = tmp_path_factory.mktemp("runner")
+    extra = {"rent_person": [1.0, 0.0]} if "rent" in present else {}
+    path = _tiny_dataset(directory / "data.h5", year=2024, extra_person=extra)
+    inputs = [
+        "age",
+        "employment_income",
+        "rent",
+        "would_claim_uc",
+        "household_weight",
+        "missing_x",
+    ]
+    snapshot = run_inputs_snapshot(
+        sys.executable,
+        critical=critical,
+        out=directory / "snapshot.json",
+        dataset_path=path,
+        model_input_variables=inputs,
+        projected_years=projected,
+    )
+    tables = dataset_columns(path)
+    _, evidence = budget_inputs(
+        make_pair(),
+        path,
+        model_input_variables=inputs,
+        critical_inputs=critical,
+        accepted_gaps={},
+        previous_missing_inputs=None,
+        previous_unusable_critical=None,
+        tables=tables,
+    )
+    assert snapshot["missing_inputs"] == evidence["missing_inputs"]
+    assert snapshot["unusable_critical"] == evidence["unusable_critical"]
+    _, years = supported_years(
+        make_pair(),
+        path,
+        required_years=[],
+        budget_horizon_years=[],
+        previous_supported_years=None,
+        projected_years=lambda _: sorted(projected),
+    )
+    assert snapshot["supported_years"] == years["supported_years"]
+
+
+STANDARD_REFORMS_FIXTURE = (
+    Path(__file__).parent / "fixtures" / "uk_metrics" / "standard_reforms.yaml"
+)
+
+
+def _tool_tarball(commit="c" * 40, include_partition=True, include_reforms=True):
+    import tarfile
+
+    buffer = io.BytesIO()
+    prefix = f"policyengine-uk-{commit}"
+    files = {
+        f"{prefix}/policyengine_uk/utils/aggregate_impact/__init__.py": b"TOOL_SCHEMA = 'x/1'\n",
+        f"{prefix}/policyengine_uk/utils/aggregate_impact/api.py": b"def compute_budget_metrics(sim, **kwargs):\n    return {}\n",
+        f"{prefix}/policyengine_uk/utils/other.py": b"not copied\n",
+    }
+    if include_partition:
+        files[f"{prefix}/policyengine_uk/data/fiscal_partition.yaml"] = b"heads: []\n"
+    if include_reforms:
+        files[
+            f"{prefix}/policyengine_uk/utils/aggregate_impact/standard_reforms.yaml"
+        ] = STANDARD_REFORMS_FIXTURE.read_bytes()
+    with tarfile.open(fileobj=buffer, mode="w:gz") as archive:
+        for name, data in files.items():
+            info = tarfile.TarInfo(name)
+            info.size = len(data)
+            archive.addfile(info, io.BytesIO(data))
+    return buffer.getvalue()
+
+
+def test_metrics_tool_is_vendored_from_one_commit(tmp_path):
+    from policyengine.provenance.uk_candidate import vendor_metrics_tool
+
+    calls = []
+
+    def get(url, timeout=None):
+        calls.append(url)
+        response = SimpleNamespace(
+            content=_tool_tarball(), raise_for_status=lambda: None
+        )
+        return response
+
+    tool = {
+        "repository": "PolicyEngine/policyengine-uk",
+        "commit": "c" * 40,
+        "path": "policyengine_uk/utils/aggregate_impact",
+    }
+    root = vendor_metrics_tool(tool, tmp_path, get=get)
+    assert calls == [
+        f"https://codeload.github.com/PolicyEngine/policyengine-uk/tar.gz/{'c' * 40}"
+    ]
+    assert sorted(p.name for p in (root / "pe_uk_aggregate_impact").iterdir()) == [
+        "__init__.py",
+        "api.py",
+        "standard_reforms.yaml",
+    ]
+    assert (root / "fiscal_partition.yaml").read_text() == "heads: []\n"
+    assert (
+        vendor_metrics_tool(
+            tool, tmp_path, get=lambda *a, **k: pytest.fail("refetched")
+        )
+        == root
+    )
+
+
+def test_metrics_report_runner_command(tmp_path):
+    import json as _json
+
+    from policyengine.provenance.uk_candidate import METRICS_RUNNER, run_metrics_report
+
+    seen = {}
+
+    def run(command, check, cwd):
+        seen["command"] = command
+        Path(command[-1]).write_text(_json.dumps({"status": "ok", "metrics": []}))
+
+    report = run_metrics_report(
+        "/venv/bin/python",
+        tmp_path / "tool",
+        years=[2026, 2027],
+        out=tmp_path / "report.json",
+        dataset_path=tmp_path / "data.h5",
+        run=run,
+    )
+    assert report == {"status": "ok", "metrics": []}
+    python, flag, code, vendor, settings, out = seen["command"]
+    assert (python, flag, code) == ("/venv/bin/python", "-c", METRICS_RUNNER)
+    assert _json.loads(settings)["years"] == [2026, 2027]
+    assert _json.loads(settings)["run_guard"] is True
+    # The runner must not pass ``reforms``: the tool then costs its own
+    # standard set, whereas an empty list would cost none.
+    assert "reforms=" not in METRICS_RUNNER
+
+
+def test_the_standard_reforms_are_read_from_the_vendored_tool(tmp_path):
+    from policyengine.provenance.uk_candidate import (
+        load_standard_reforms,
+        vendor_metrics_tool,
+    )
+
+    tool = {
+        "repository": "PolicyEngine/policyengine-uk",
+        "commit": "c" * 40,
+        "path": "policyengine_uk/utils/aggregate_impact",
+    }
+    root = vendor_metrics_tool(
+        tool,
+        tmp_path,
+        get=lambda url, timeout=None: SimpleNamespace(
+            content=_tool_tarball(), raise_for_status=lambda: None
+        ),
+    )
+    reforms = load_standard_reforms(root, "standard_reforms.yaml")
+    assert len(reforms) == 10
+    assert reforms[0] == {
+        "id": "basic_rate_plus_1pp",
+        "name": "Raise basic rate by 1pp",
+        "variable": "income_tax",
+        "direction": "up",
+        "minimum": 1.2e9,
+    }
+    assert {reform["id"] for reform in reforms} >= {
+        "uc_standard_allowance_plus_10pc",
+        "state_pension_plus_10pc",
+    }
+    with pytest.raises(ValueError, match="ships no"):
+        load_standard_reforms(root, "missing.yaml")
+
+
+@pytest.mark.parametrize(
+    "entry",
+    [
+        {
+            "name": "No id",
+            "responds": {"variable": "x", "direction": "up", "minimum": 1},
+        },
+        {"id": "no_response", "name": "No response"},
+        {
+            "id": "bad",
+            "responds": {"variable": "x", "direction": "sideways", "minimum": 1},
+        },
+        {"id": "zero", "responds": {"variable": "x", "direction": "up", "minimum": 0}},
+    ],
+    ids=["no id", "no response", "bad direction", "zero minimum"],
+)
+def test_a_standard_reform_without_a_stated_response_is_refused(tmp_path, entry):
+    import yaml
+
+    from policyengine.provenance.uk_candidate import load_standard_reforms
+
+    package = tmp_path / "pe_uk_aggregate_impact"
+    package.mkdir()
+    (package / "standard_reforms.yaml").write_text(yaml.safe_dump({"reforms": [entry]}))
+    with pytest.raises(ValueError):
+        load_standard_reforms(tmp_path, "standard_reforms.yaml")
+
+
+# --- Reform responsiveness -----------------------------------------------------------
+
+RESPONSE_YEARS = [2025, 2026, 2027, 2028, 2029, 2030, 2031]
+
+
+def _standard_reforms(tmp_path_factory=None):
+    import yaml
+
+    return [
+        {
+            "id": entry["id"],
+            "name": entry["name"],
+            "variable": entry["responds"]["variable"],
+            "direction": entry["responds"]["direction"],
+            "minimum": float(entry["responds"]["minimum"]),
+        }
+        for entry in yaml.safe_load(STANDARD_REFORMS_FIXTURE.read_text())["reforms"]
+    ]
+
+
+def _tool_check(reform, changes):
+    """The tool's responsive verdict, restated from reforms.responsiveness()."""
+
+    sign = 1.0 if reform["direction"] == "up" else -1.0
+    return bool(changes) and all(
+        sign * change >= reform["minimum"] for change in changes.values()
+    )
+
+
+def _responsive_report(scale=5.0, overrides=None, years=RESPONSE_YEARS, lie=None):
+    """A report in which each reform moves its target by scale x its minimum.
+
+    ``overrides`` replaces single values (None removes the row). The
+    ``responsive`` checks follow the tool's rule, unless ``lie`` names a
+    reform id whose check is inverted.
+    """
+
+    overrides = overrides or {}
+    metrics, identities = [], []
+    for reform in _standard_reforms():
+        sign = 1.0 if reform["direction"] == "up" else -1.0
+        changes = {}
+        for year in years:
+            value = overrides.get(
+                (reform["id"], year), sign * reform["minimum"] * scale
+            )
+            if value is not None:
+                changes[year] = value
+                metrics.append(
+                    {
+                        "metric_id": f"reform_targets.{reform['id']}",
+                        "year": year,
+                        "value": value,
+                        "kind": "reform_target",
+                        **PUBLISHED,
+                    }
+                )
+        finite = {y: v for y, v in changes.items() if v == v}
+        ok = len(finite) == len(years) and _tool_check(reform, finite)
+        if lie == reform["id"]:
+            ok = not ok
+        identities.append(
+            {"identity_id": f"reform_runs:{reform['id']}", "status": "pass"}
+        )
+        identities.append(
+            {
+                "identity_id": f"responsive:{reform['id']}",
+                "status": "pass" if ok else "fail",
+                "detail": "" if ok else f"expected {reform['variable']} to move",
+            }
+        )
+    return {
+        "status": "ok",
+        "years": list(years),
+        "metrics": metrics,
+        "identities": identities,
+    }
+
+
+def _responsiveness(report, reforms=None, years=RESPONSE_YEARS):
+    from policyengine.provenance.uk_candidate import reform_responsiveness
+
+    return reform_responsiveness(
+        report=report,
+        standard_reforms=_standard_reforms() if reforms is None else reforms,
+        years=years,
+    )[0]
+
+
+def test_reforms_that_all_respond_pass():
+    result = _responsiveness(_responsive_report())
+    assert result.status == "pass", result.summary
+    assert "All 10 standard reforms" in result.summary
+    assert "2025-26 to 2031-32" in result.summary
+
+
+UC_ALLOWANCE = "uc_standard_allowance_plus_10pc"
+STATE_PENSION = "state_pension_plus_10pc"
+
+
+@pytest.mark.parametrize(
+    ("overrides", "reform_id"),
+    [
+        ({(UC_ALLOWANCE, year): 0.0 for year in RESPONSE_YEARS}, UC_ALLOWANCE),
+        ({(STATE_PENSION, year): 4.0e7 for year in RESPONSE_YEARS}, STATE_PENSION),
+        ({(UC_ALLOWANCE, 2027): 0.0}, UC_ALLOWANCE),
+        ({(STATE_PENSION, 2031): -1.3e10}, STATE_PENSION),
+        ({("basic_rate_plus_1pp", 2030): None}, "basic_rate_plus_1pp"),
+        ({("basic_rate_plus_1pp", 2030): float("nan")}, "basic_rate_plus_1pp"),
+    ],
+    ids=[
+        "exactly zero",
+        "nearly inert",
+        "one inert year",
+        "wrong sign",
+        "missing",
+        "nan",
+    ],
+)
+def test_a_reform_that_does_not_respond_fails_qualification(overrides, reform_id):
+    """The backtest's two modes (exactly £0, nearly inert) and their neighbours."""
+
+    result = _responsiveness(_responsive_report(overrides=overrides))
+    assert result.status == "fail"
+    assert f"({reform_id}) did not respond" in result.summary, result.summary
+
+
+def test_a_run_that_costed_no_reforms_cannot_pass():
+    """Presence is required: an empty or partial check set is a failure."""
+
+    report = _responsive_report()
+    report["identities"] = []
+    result = _responsiveness(report)
+    assert result.status == "fail" and "did not run" in result.summary
+    report = _responsive_report()
+    report["identities"] = [
+        row
+        for row in report["identities"]
+        if row["identity_id"] != f"responsive:{STATE_PENSION}"
+    ]
+    result = _responsiveness(report)
+    assert result.status == "fail"
+    assert "no responsive check in the report" in result.summary
+    report = _responsive_report()
+    for row in report["identities"]:
+        if row["identity_id"] == f"reform_runs:{UC_ALLOWANCE}":
+            row["status"] = "fail"
+    assert f"({UC_ALLOWANCE}) did not run" in _responsiveness(report).summary
+
+
+def test_the_tools_verdict_is_checked_against_its_own_numbers():
+    """Differential: a pass the reform_targets values do not support is an error."""
+
+    inert = {(UC_ALLOWANCE, year): 0.0 for year in RESPONSE_YEARS}
+    lying_pass = _responsive_report(overrides=inert, lie=UC_ALLOWANCE)
+    result = _responsiveness(lying_pass)
+    assert result.status == "error"
+    assert "do not meet the minimum" in result.summary
+    lying_fail = _responsive_report(lie=STATE_PENSION)
+    assert _responsiveness(lying_fail).status == "fail"
+    short = _responsive_report(years=RESPONSE_YEARS[:-1])
+    assert _responsiveness(short).status in ("fail", "error")
+
+
+@pytest.mark.parametrize("direction", ["up", "down"])
+def test_a_large_move_in_the_wrong_direction_is_not_a_response(direction):
+    """A reform that moves its target the wrong way, by far more than the
+    minimum, has not responded, whatever the tool's verdict says. The
+    magnitude alone must never pass: the comparison is signed."""
+
+    reform = next(r for r in _standard_reforms() if r["direction"] == direction)
+    sign = 1.0 if direction == "up" else -1.0
+    wrong_way = {
+        (reform["id"], year): -sign * reform["minimum"] * 5.0 for year in RESPONSE_YEARS
+    }
+    honest = _responsiveness(_responsive_report(overrides=wrong_way))
+    assert honest.status == "fail"
+    assert f"({reform['id']}) did not respond" in honest.summary, honest.summary
+    lying_pass = _responsiveness(
+        _responsive_report(overrides=wrong_way, lie=reform["id"])
+    )
+    assert lying_pass.status == "error", lying_pass.summary
+    assert "do not meet the minimum" in lying_pass.summary
+
+
+def test_responsiveness_is_never_skipped_or_guessed():
+    from policyengine.provenance.qualification import (
+        REQUIRED_GATES,
+        GateResult,
+        qualification_verdict,
+    )
+
+    assert _responsiveness(None).status == "unavailable"
+    assert _responsiveness(_responsive_report(), reforms=[]).status == "unavailable"
+    assert "reform_responsiveness" in REQUIRED_GATES
+    for status, verdict in (("unavailable", "incomplete"), ("fail", "rejected")):
+        gates = [
+            GateResult(
+                gate_id=name,
+                status=status if name == "reform_responsiveness" else "pass",
+                summary="x",
+            )
+            for name in REQUIRED_GATES
+        ]
+        assert qualification_verdict(gates) == verdict
+    missing = [
+        GateResult(gate_id=name, status="pass", summary="x")
+        for name in REQUIRED_GATES
+        if name != "reform_responsiveness"
+    ]
+    assert qualification_verdict(missing) == "incomplete"
+
+
+@settings(max_examples=100, deadline=None)
+@given(
+    low=st.floats(min_value=-3.0, max_value=6.0),
+    high=st.floats(min_value=-3.0, max_value=6.0),
+)
+def test_responsiveness_is_monotone_in_the_size_of_the_response(low, high):
+    """Shrinking every response, or flipping its sign, never turns fail into pass."""
+
+    low, high = sorted((low, high))
+    order = {"fail": 0, "pass": 1}
+    weaker = _responsiveness(_responsive_report(scale=low)).status
+    stronger = _responsiveness(_responsive_report(scale=high)).status
+    assert order[weaker] <= order[stronger]
+    assert (weaker == "pass") == (low >= 1.0)
+    assert (stronger == "pass") == (high >= 1.0)
+
+
+def _withhold(report, reform_id, years):
+    """Publish ``reform_id``'s target change as suppressed in ``years``, as
+    the tool does for a change from fewer than 10 records."""
+
+    for row in report["metrics"]:
+        if row["metric_id"] == f"reform_targets.{reform_id}" and row["year"] in years:
+            row.update(value=None, suppressed=True)
+    return report
+
+
+def test_a_withheld_year_cannot_pass_unverified():
+    """W3's rule 5: the tool decides responsiveness from the withheld value;
+    the gate cannot recompute it, so a pass there needs review."""
+
+    report = _withhold(_responsive_report(), UC_ALLOWANCE, {2027})
+    result, evidence = reform_responsiveness_gate(report)
+    assert result.status == "needs_review", result.summary
+    assert f"({UC_ALLOWANCE})" in result.summary and "2027-28" in result.summary
+    rows = {row["id"]: row for row in evidence["reforms"]}
+    assert rows[UC_ALLOWANCE]["withheld_years"] == [2027]
+    assert rows[UC_ALLOWANCE]["target_changes"]["2027"] is None
+    # Withholding every year is no better.
+    everything = _withhold(_responsive_report(), UC_ALLOWANCE, set(RESPONSE_YEARS))
+    assert reform_responsiveness_gate(everything)[0].status == "needs_review"
+
+
+def test_a_withheld_year_never_excuses_a_failure():
+    # The tool's failing verdict may come from the withheld year: it fails.
+    failing = _withhold(_responsive_report(), UC_ALLOWANCE, {2027})
+    for row in failing["identities"]:
+        if row["identity_id"] == f"responsive:{UC_ALLOWANCE}":
+            row.update(status="fail", detail="2027-28: did not move enough")
+    result = reform_responsiveness_gate(failing)[0]
+    assert (
+        result.status == "fail"
+        and f"({UC_ALLOWANCE}) did not respond" in result.summary
+    )
+    # A published year that misses the minimum, under a passing verdict, is
+    # still a disagreement, withheld years or not.
+    lying = _withhold(
+        _responsive_report(overrides={(UC_ALLOWANCE, 2028): 0.0}, lie=UC_ALLOWANCE),
+        UC_ALLOWANCE,
+        {2027},
+    )
+    assert reform_responsiveness_gate(lying)[0].status == "error"
+    # A null that is not marked suppressed is a missing value, not withheld.
+    unmarked = _responsive_report()
+    for row in unmarked["metrics"]:
+        if row["metric_id"] == f"reform_targets.{UC_ALLOWANCE}" and row["year"] == 2027:
+            row["value"] = None
+    assert reform_responsiveness_gate(unmarked)[0].status == "error"
+
+
+def test_a_suppressed_reform_target_with_a_value_is_never_used_or_recorded():
+    """supp-r1 review: a target marked suppressed but carrying a value (a
+    synthetic one here) must not pass, and its value never reaches the
+    evidence."""
+
+    report = _responsive_report()
+    for row in report["metrics"]:
+        if row["metric_id"] == f"reform_targets.{UC_ALLOWANCE}" and row["year"] == 2027:
+            row["suppressed"] = True  # keeps its (passing) value
+    result, evidence = reform_responsiveness_gate(report)
+    assert result.status == "error"
+    assert (
+        f"suppressed reform targets carry a value: reform_targets.{UC_ALLOWANCE} 2027"
+        in (result.summary)
+    )
+    rows = {row["id"]: row for row in evidence["reforms"]}
+    assert rows[UC_ALLOWANCE]["target_changes"]["2027"] is None
+    assert rows[UC_ALLOWANCE]["withheld_years"] == []
+
+
+@pytest.mark.parametrize("what", ["target", "reform_runs", "responsive"])
+def test_a_repeated_reform_row_cannot_decide_responsiveness(what):
+    report = _responsive_report()
+    if what == "target":
+        row = next(
+            r
+            for r in report["metrics"]
+            if r["metric_id"] == f"reform_targets.{UC_ALLOWANCE}"
+        )
+        report["metrics"].append({**row, "value": 0.0})
+        repeated = f"reform_targets.{UC_ALLOWANCE} {row['year']}"
+    else:
+        check = f"{what}:{UC_ALLOWANCE}"
+        report["identities"].append({"identity_id": check, "status": "pass"})
+        repeated = check
+    result = reform_responsiveness_gate(report)[0]
+    assert (
+        result.status == "error" and f"the report repeats {repeated}" in result.summary
+    )
+
+
+def test_a_boolean_reform_target_is_not_a_number():
+    """supp-r1 mutant M21: True must not count as a change of 1."""
+
+    reform = {**_standard_reforms()[0], "minimum": 0.5}
+    report = {
+        "status": "ok",
+        "years": [2027],
+        "metrics": [
+            {
+                "metric_id": f"reform_targets.{reform['id']}",
+                "year": 2027,
+                "value": True,
+                **PUBLISHED,
+            }
+        ],
+        "identities": [
+            {"identity_id": f"reform_runs:{reform['id']}", "status": "pass"},
+            {"identity_id": f"responsive:{reform['id']}", "status": "pass"},
+        ],
+    }
+    from policyengine.provenance.uk_candidate import reform_responsiveness
+
+    result = reform_responsiveness(
+        report=report, standard_reforms=[reform], years=[2027]
+    )[0]
+    assert result.status == "error" and "do not meet the minimum" in result.summary
+
+
+@settings(max_examples=150, deadline=None)
+@given(
+    withheld=st.sets(st.sampled_from(RESPONSE_YEARS)),
+    scale=st.floats(min_value=-3.0, max_value=6.0),
+)
+def test_withholding_never_improves_responsiveness(withheld, scale):
+    """Monotone in information: withholding years never turns a fail or an
+    error into a pass, and never passes at all. Only the State Pension
+    reform varies; the other nine respond, so its own verdict decides."""
+
+    order = {"pass": 0, "needs_review": 1, "fail": 2, "error": 2}
+    pension = next(r for r in _standard_reforms() if r["id"] == STATE_PENSION)
+    sign = 1.0 if pension["direction"] == "up" else -1.0
+    overrides = {
+        (STATE_PENSION, year): sign * pension["minimum"] * scale
+        for year in RESPONSE_YEARS
+    }
+    full = reform_responsiveness_gate(_responsive_report(overrides=overrides))[0].status
+    hidden = reform_responsiveness_gate(
+        _withhold(_responsive_report(overrides=overrides), STATE_PENSION, withheld)
+    )[0].status
+    assert full == ("pass" if scale >= 1.0 else "fail")
+    if withheld:
+        assert hidden != "pass"
+    assert order[hidden] >= order[full]
+
+
+REAL_REPORT = (
+    Path(__file__).parent / "fixtures" / "uk_metrics" / "budget_metrics_report.json"
+)
+
+
+def _real_report():
+    return json.loads(REAL_REPORT.read_text())
+
+
+def test_the_real_report_on_the_certified_data_passes_responsiveness():
+    """W3's real report: ten reforms, each clearing its minimum in every year."""
+
+    report = _real_report()
+    result, evidence = reform_responsiveness_gate(report)
+    assert result.status == "pass", result.summary
+    assert "All 10 standard reforms" in result.summary
+    rows = {row["id"]: row for row in evidence["reforms"]}
+    uc = rows["uc_standard_allowance_plus_10pc"]["target_changes"]
+    pension = rows["state_pension_plus_10pc"]["target_changes"]
+    assert all(3.0e9 < value < 3.4e9 for value in uc.values())
+    assert all(12.0e9 < value < 14.5e9 for value in pension.values())
+    assert all(row["withheld_years"] == [] for row in rows.values())
+
+
+def reform_responsiveness_gate(report):
+    from policyengine.provenance.uk_candidate import reform_responsiveness
+
+    return reform_responsiveness(
+        report=report, standard_reforms=_standard_reforms(), years=RESPONSE_YEARS
+    )
+
+
+@pytest.mark.parametrize("reform_id", [UC_ALLOWANCE, STATE_PENSION])
+def test_the_backtest_failures_fail_on_the_real_report(reform_id):
+    """Nikhil's backtest: UC allowance +10% returned exactly £0; State Pension +10% barely moved."""
+
+    report = _real_report()
+    for row in report["metrics"]:
+        if row["metric_id"] == f"reform_targets.{reform_id}":
+            row["value"] = 0.0 if reform_id == UC_ALLOWANCE else row["value"] * 0.01
+    # The tool's own check would say fail; a report that still says pass is an error.
+    assert reform_responsiveness_gate(report)[0].status == "error"
+    for row in report["identities"]:
+        if row["identity_id"] == f"responsive:{reform_id}":
+            row["status"] = "fail"
+            row["detail"] = "did not move"
+    result = reform_responsiveness_gate(report)[0]
+    assert result.status == "fail"
+    assert f"({reform_id}) did not respond" in result.summary
+
+
+def _real_bounds():
+    return json.loads(
+        (
+            Path(__file__).parents[1]
+            / "src/policyengine/data/qualification/uk_review_bounds.json"
+        ).read_text()
+    )
+
+
+def test_the_real_report_fits_the_metrics_review_classification():
+    """Every check that fails on the certified pair is a no-worsening check,
+    and every metric kind has a review bound, so a pair compared with itself
+    passes, suppressed figures and all."""
+
+    from policyengine.provenance.uk_candidate import classify_check
+
+    report = _real_report()
+    bounds = _real_bounds()
+    failing = sorted(
+        row["identity_id"] for row in report["identities"] if row["status"] == "fail"
+    )
+    assert failing == ["sign:vat"]
+    assert {classify_check(check_id) for check_id in failing} == {"regression"}
+    assert {row["kind"] for row in report["metrics"]} <= set(bounds["kinds"])
+    status, summary, evidence = compare_metric_reports(report, report, bounds)
+    assert status == "pass", summary
+    assert "14 suppressed on both sides, not compared" in summary
+    assert sorted(evidence["preexisting_check_failures"]) == failing
+    assert evidence["skipped_checks"] == ["sign:disabled_students_allowance"]
+    assert evidence["newly_skipped_checks"] == evidence["hard_checks_not_run"] == []
+    assert evidence["withheld_changes"] == []
+    assert len(evidence["suppressed_metrics"]) == 14
+    # The same failures against a pair that passed them are regressions.
+    clean = _real_report()
+    for row in clean["identities"]:
+        if row["status"] == "fail":
+            row["status"] = "pass"
+    assert compare_metric_reports(report, clean, bounds)[0] == "fail"
+
+
+def test_the_real_report_suppresses_its_small_cells():
+    """W3's report on the certified pair: Disabled Students' Allowance comes
+    from fewer than 10 records, so its total and caseload are suppressed in
+    every year, every total holding it is rebuilt without it, and its sign
+    check is not run. A report that published it would fail the gate."""
+
+    report = _real_report()
+    suppressed = sorted(
+        (row["metric_id"], row["year"])
+        for row in report["metrics"]
+        if row["suppressed"]
+    )
+    dsa = (
+        "spending.disabled_students_allowance",
+        "caseloads.disabled_students_allowance",
+    )
+    assert suppressed == sorted((m, y) for m in dsa for y in RESPONSE_YEARS)
+    assert all(row["value"] is None for row in report["metrics"] if row["suppressed"])
+    rebuilt = [row for row in report["metrics"] if row["excludes"]]
+    assert len(rebuilt) == 112
+    assert len({row["metric_id"] for row in rebuilt}) == 16
+    assert {tuple(row["excludes"]) for row in rebuilt} == {(dsa[0],)}
+    # A figure published from a small cell, or a total that names one it
+    # does not suppress, is a report the gate cannot read safely.
+    published = _real_report()
+    for row in published["metrics"]:
+        if row["metric_id"] == dsa[0] and row["year"] == 2027:
+            row["value"] = 1.0e6
+    status, summary, _ = compare_metric_reports(published, report, _real_bounds())
+    assert status == "fail"
+    assert f"suppressed figures with a value: {dsa[0]} 2027" in summary
+    unsuppressed = _real_report()
+    for row in unsuppressed["metrics"]:
+        if row["metric_id"] == dsa[0] and row["year"] == 2027:
+            row.update(value=1.0e6, suppressed=False)
+    status, summary, _ = compare_metric_reports(unsuppressed, report, _real_bounds())
+    assert status == "fail"
+    assert "totals excluding a figure it does not suppress" in summary
+
+
+def test_the_real_report_gives_each_excluded_figure_its_sign():
+    """At 1191ac5a4 Disabled Students' Allowance enters the spending totals,
+    household benefits and household net income at +1, and the two balances
+    at -1."""
+
+    signs = {
+        row["metric_id"]: row["exclude_signs"]
+        for row in _real_report()["metrics"]
+        if row["excludes"] and row["year"] == 2027
+    }
+    dsa = "spending.disabled_students_allowance"
+    assert {metric for metric, sign in signs.items() if sign == {dsa: -1}} == {
+        "model.gov_balance",
+        "balance.balance",
+    }
+    assert all(sign in ({dsa: 1}, {dsa: -1}) for sign in signs.values())
+
+
+def test_a_head_published_in_the_previous_pair_lines_up_on_the_real_report():
+    """A previous pair that published Disabled Students' Allowance in
+    2027-28 (a synthetic £5m and 1,000 claimants, not data): its totals hold
+    it, the candidate's are rebuilt without it. The six additive totals line
+    up exactly; the head, its caseload and the ten decile means are
+    withheld."""
+
+    candidate, previous = _real_report(), _real_report()
+    dsa = "spending.disabled_students_allowance"
+    synthetic = {dsa: 5.0e6, "caseloads.disabled_students_allowance": 1.0e3}
+    for row in previous["metrics"]:
+        if row["year"] != 2027:
+            continue
+        if row["metric_id"] in synthetic:
+            row.update(value=synthetic[row["metric_id"]], suppressed=False)
+        elif row["excludes"]:
+            row["value"] += row["exclude_signs"][dsa] * synthetic[dsa]
+            row.update(excludes=[], exclude_signs={})
+    status, summary, evidence = compare_metric_reports(
+        candidate, previous, _real_bounds()
+    )
+    assert status == "needs_review", summary
+    assert sorted(evidence["lined_up_totals"]) == sorted(
+        f"{metric} 2027"
+        for metric in (
+            "balance.balance",
+            "balance.spending",
+            "household.household_net_income",
+            "model.gov_balance",
+            "model.gov_spending",
+            "model.household_benefits",
+        )
+    )
+    withheld = sorted(row["metric_id"] for row in evidence["withheld_changes"])
+    assert withheld == sorted(
+        [dsa, "caseloads.disabled_students_allowance"]
+        + [f"deciles.mean_income.{decile}" for decile in range(1, 11)]
+    )
+    assert evidence["moves_beyond_bounds"] == []
+
+
+def test_only_a_well_formed_report_is_published_to_the_ledger():
+    """The ledger is public: a report that breaks the small-cell format is
+    replaced by its digest and problems (supp-r1 review)."""
+
+    from policyengine.provenance.uk_candidate import publishable_metrics_report
+
+    report = _real_report()
+    assert publishable_metrics_report(report) == report
+    marker = 123456789.25  # synthetic
+    leaked = _real_report()
+    for row in leaked["metrics"]:
+        if row["metric_id"] == "spending.disabled_students_allowance":
+            row["value"] = marker
+    published = publishable_metrics_report(leaked)
+    assert set(published) == {"withheld", "report_sha256", "problems"}
+    assert "suppressed figures with a value" in published["problems"][0]
+    assert str(marker) not in json.dumps(published)
+    # A report from a tool that predates suppression is not published either.
+    old = _real_report()
+    for row in old["metrics"]:
+        for field in ("suppressed", "excludes", "exclude_signs"):
+            row.pop(field)
+        row["value"] = row["value"] if row["value"] is not None else marker
+    assert "withheld" in publishable_metrics_report(old)
+    assert str(marker) not in json.dumps(publishable_metrics_report(old))
+
+
+def test_a_real_failure_that_spread_to_a_new_year_has_worsened():
+    """Scope-based no-worsening on the tool's real failing_scopes."""
+
+    bounds = _real_bounds()
+    report = _real_report()
+    scopes = {
+        row["identity_id"]: row["failing_scopes"]
+        for row in report["identities"]
+        if row["status"] == "fail"
+    }
+    assert scopes == {"sign:vat": [str(year) for year in RESPONSE_YEARS]}
+    # The previous pair failed sign:vat in every year but the last.
+    earlier = _real_report()
+    for row in earlier["identities"]:
+        if row["identity_id"] == "sign:vat":
+            row["failing_scopes"] = row["failing_scopes"][:-1]
+    status, summary, _ = compare_metric_reports(report, earlier, bounds)
+    assert status == "fail" and "sign:vat (fails in more scopes)" in summary
+    # A candidate that fails it in fewer years than before has not worsened.
+    assert compare_metric_reports(earlier, report, bounds)[0] == "pass"
+
+
+def test_reform_checks_are_hard_in_the_metrics_review():
+    from policyengine.provenance.uk_candidate import classify_check
+
+    assert classify_check("reform_runs:basic_rate_plus_1pp") == "hard"
+    assert classify_check("responsive:state_pension_plus_10pc") == "hard"
+
+
+def test_metrics_with_nan_values_fail_rather_than_crash():
+    nan = {**BASE, ("income_tax", 2027, "receipt"): float("nan")}
+    result, evidence = metrics_review(
+        candidate_report=_report(nan), previous_report=_report(BASE), bounds=BOUNDS
+    )
+    assert result.status == "fail"
+    assert result.evidence_sha256
diff --git a/tests/test_uk_candidate_metric_baselines.py b/tests/test_uk_candidate_metric_baselines.py
new file mode 100644
index 00000000..b55fc15e
--- /dev/null
+++ b/tests/test_uk_candidate_metric_baselines.py
@@ -0,0 +1,743 @@
+"""Review bounds require usable baseline values for each candidate metric."""
+
+import copy
+
+import pytest
+
+from policyengine.provenance.uk_candidate import compare_metric_reports, metrics_review
+
+BOUNDS = {"default": {"relative": 0.02, "absolute": 5e8}}
+
+
+#: The small-cell fields every row of the pinned tool's report carries.
+PUBLISHED = {"suppressed": False, "excludes": [], "exclude_signs": {}}
+
+
+def _report():
+    return {
+        "status": "ok",
+        "metrics_version": "m1",
+        "partition_version": "p1",
+        "metrics": [
+            {
+                "metric_id": "income_tax",
+                "year": 2027,
+                "kind": "receipt",
+                "value": 3e11,
+                **PUBLISHED,
+            },
+            {
+                "metric_id": "relative_poverty",
+                "year": 2027,
+                "kind": "poverty",
+                "value": 0.17,
+                **PUBLISHED,
+            },
+        ],
+        "identities": [{"identity_id": "weights:household", "status": "pass"}],
+    }
+
+
+@pytest.mark.parametrize(
+    "baseline_value", [float("nan"), float("inf"), -float("inf"), None, "invalid"]
+)
+def test_unusable_baseline_cannot_claim_a_large_move_is_within_bounds(baseline_value):
+    candidate, previous = _report(), _report()
+    candidate["metrics"][0]["value"] = 1e12
+    previous["metrics"][0]["value"] = baseline_value
+
+    status, summary, _ = compare_metric_reports(candidate, previous, BOUNDS)
+
+    assert status == "fail"
+    assert "previous" in summary and "non-finite" in summary
+    gate, _ = metrics_review(
+        candidate_report=candidate, previous_report=previous, bounds=BOUNDS
+    )
+    assert gate.status == "fail"
+    assert gate.evidence_sha256
+
+
+def test_a_baseline_row_without_a_value_cannot_pass():
+    candidate, previous = _report(), _report()
+    del previous["metrics"][0]["value"]
+    assert compare_metric_reports(candidate, previous, BOUNDS)[0] == "fail"
+
+
+def test_a_metric_without_a_baseline_requires_review():
+    candidate, previous = _report(), _report()
+    previous["metrics"] = previous["metrics"][1:]
+    status, summary, evidence = compare_metric_reports(candidate, previous, BOUNDS)
+    assert status == "needs_review"
+    assert "no baseline" in summary and "income_tax" in summary
+    assert evidence["metrics_without_baseline"] == ["income_tax 2027"]
+
+
+@pytest.mark.parametrize("side", ["candidate", "previous"])
+def test_a_duplicate_metric_cannot_hide_an_unusable_value(side):
+    candidate, previous = _report(), _report()
+    report = candidate if side == "candidate" else previous
+    report["metrics"].insert(0, {**report["metrics"][0], "value": float("nan")})
+    status, summary, _ = compare_metric_reports(candidate, previous, BOUNDS)
+    assert status == "fail"
+    assert "duplicate metrics" in summary and side in summary
+
+
+def test_duplicate_finite_baselines_cannot_select_the_most_favourable_comparison():
+    candidate, previous = _report(), _report()
+    candidate["metrics"][0]["value"] = 1e12
+    previous["metrics"].append({**previous["metrics"][0], "value": 1e12})
+    status, summary, _ = compare_metric_reports(candidate, previous, BOUNDS)
+    assert status == "fail"
+    assert "previous report has duplicate metrics: income_tax 2027" in summary
+
+
+def test_an_unusable_cross_report_cannot_pass_either_review_step():
+    candidate, previous, cross = _report(), _report(), _report()
+    cross["metrics"][0]["value"] = float("nan")
+    gate, evidence = metrics_review(
+        candidate_report=candidate,
+        previous_report=previous,
+        cross_report=cross,
+        bounds=BOUNDS,
+    )
+    assert gate.status == "fail"
+    assert "model_only" in evidence and "data_only" in evidence
+    assert "data_only: the previous report has non-finite" in gate.summary
+
+
+def test_valid_baselines_preserve_bounds_and_do_not_mutate_reports():
+    candidate, previous = _report(), _report()
+    original = copy.deepcopy((candidate, previous))
+    assert compare_metric_reports(candidate, previous, BOUNDS)[0] == "pass"
+    assert (candidate, previous) == original
+    candidate["metrics"][0]["value"] = 1e12
+    status, _, evidence = compare_metric_reports(candidate, previous, BOUNDS)
+    assert status == "needs_review"
+    assert evidence["moves_beyond_bounds"][0]["metric_id"] == "income_tax"
+
+
+def _previous_certificate():
+    return {
+        "bundle_version": "6.3.0",
+        "model_version": "2.125.1",
+        "core_version": "3.33.0",
+        "model_wheel_sha256": "1" * 64,
+        "core_wheel_sha256": "2" * 64,
+        "data_release_version": "1.58.0",
+        "dataset_sha256": "3" * 64,
+        "dataset_name": "national",
+        "release_manifest_reference": {
+            "repo_id": "policyengine/uk-data",
+            "repo_type": "model",
+            "path": "releases/1.58.0/release_manifest.json",
+            "revision": "b" * 40,
+        },
+        "dataset_reference": {
+            "repo_id": "policyengine/uk-data",
+            "repo_type": "model",
+            "path": "national.h5",
+            "revision": "a" * 40,
+            "sha256": "3" * 64,
+        },
+    }
+
+
+def _previous_install_io(
+    tmp_path, monkeypatch, *, bad_pypi=None, bad_download=None, identity_problems=None
+):
+    import json
+    from pathlib import Path
+    from types import SimpleNamespace
+
+    import requests
+
+    from policyengine.provenance import uk_candidate
+
+    previous = _previous_certificate()
+    commands = []
+
+    def get(url, **kwargs):
+        package, version = url.removeprefix("https://pypi.org/pypi/").split("/")[:2]
+        digest = previous[
+            "model_wheel_sha256"
+            if package == "policyengine-uk"
+            else "core_wheel_sha256"
+        ]
+        if package == bad_pypi:
+            digest = "0" * 64
+        return SimpleNamespace(
+            raise_for_status=lambda: None,
+            json=lambda: {
+                "urls": [
+                    {
+                        "packagetype": "bdist_wheel",
+                        "filename": f"{package.replace('-', '_')}-{version}-py3-none-any.whl",
+                        "url": f"https://wheels/{package}/{version}",
+                        "digests": {"sha256": digest},
+                    }
+                ]
+            },
+        )
+
+    def run(command, **kwargs):
+        commands.append(command)
+        if command[:2] == ["uv", "venv"]:
+            python = Path(command[2]) / "bin/python"
+            python.parent.mkdir(parents=True)
+            python.touch()
+        elif "-c" in command:
+            Path(command[-1]).write_text(
+                json.dumps({"problems": identity_problems or []})
+            )
+
+    monkeypatch.setattr(requests, "get", get)
+    monkeypatch.setattr(uk_candidate, "_download", lambda url, path, **kwargs: path)
+    monkeypatch.setattr(
+        uk_candidate,
+        "sha256_file",
+        lambda path: (
+            "0" * 64
+            if (
+                ("policyengine_uk" in path.name and bad_download == "policyengine-uk")
+                or (
+                    "policyengine_core" in path.name
+                    and bad_download == "policyengine-core"
+                )
+            )
+            else previous[
+                "model_wheel_sha256"
+                if "policyengine_uk" in path.name
+                else "core_wheel_sha256"
+            ]
+        ),
+    )
+    return previous, commands, run
+
+
+def test_previous_baseline_installs_the_certificate_pair_not_the_wrapper_extra(
+    tmp_path, monkeypatch
+):
+    from policyengine.provenance import uk_candidate
+
+    previous, commands, run = _previous_install_io(tmp_path, monkeypatch)
+    uk_candidate.previous_pair_python(previous, tmp_path, run=run)
+    install = next(
+        command for command in commands if command[:3] == ["uv", "pip", "install"]
+    )
+    assert f"policyengine=={previous['bundle_version']}" in install
+    assert not any(argument.startswith("policyengine[uk]") for argument in install)
+    assert any("policyengine_uk-2.125.1" in argument for argument in install)
+    assert any("policyengine_core-3.33.0" in argument for argument in install)
+    assert any("-c" in command for command in commands), (
+        "The installed wheels must be verified."
+    )
+
+
+@pytest.mark.parametrize("package", ["policyengine-uk", "policyengine-core"])
+@pytest.mark.parametrize("source", ["PyPI", "download"])
+def test_previous_baseline_refuses_a_different_wheel_hash(
+    tmp_path, monkeypatch, package, source
+):
+    from policyengine.provenance import uk_candidate
+
+    previous, _, run = _previous_install_io(
+        tmp_path,
+        monkeypatch,
+        bad_pypi=package if source == "PyPI" else None,
+        bad_download=package if source == "download" else None,
+    )
+    result, _ = uk_candidate.run_gate(
+        "metrics_review",
+        lambda: uk_candidate.previous_pair_python(previous, tmp_path, run=run),
+    )
+    assert result.status == "unavailable"
+    assert package in result.summary and "wheel differs" in result.summary
+
+
+def test_previous_baseline_refuses_installed_mismatches_and_rechecks_reused_environments(
+    tmp_path, monkeypatch
+):
+    from policyengine.provenance import uk_candidate
+
+    previous, commands, run = _previous_install_io(tmp_path, monkeypatch)
+    first = uk_candidate.previous_pair_python(previous, tmp_path, run=run)
+    second = uk_candidate.previous_pair_python(previous, tmp_path, run=run)
+    assert first == second
+    assert sum(command[:2] == ["uv", "venv"] for command in commands) == 1
+    assert sum("-c" in command for command in commands) == 2
+    previous, _, run = _previous_install_io(
+        tmp_path,
+        monkeypatch,
+        identity_problems=["policyengine-core: installed bytes differ"],
+    )
+    result, _ = uk_candidate.run_gate(
+        "metrics_review",
+        lambda: uk_candidate.previous_pair_python(previous, tmp_path, run=run),
+    )
+    assert result.status == "unavailable"
+    assert "installed bytes differ" in result.summary
+
+
+def test_previous_baseline_cache_changes_when_same_wrapper_certifies_a_new_pair(
+    tmp_path, monkeypatch
+):
+    from policyengine.provenance import uk_candidate
+
+    previous, _, run = _previous_install_io(tmp_path, monkeypatch)
+    first = uk_candidate.previous_pair_python(previous, tmp_path, run=run)
+    previous["model_version"] = "2.126.12"
+    second = uk_candidate.previous_pair_python(previous, tmp_path, run=run)
+    assert first != second
+
+
+@pytest.mark.parametrize(
+    "changed_pin",
+    [
+        "model_version",
+        "core_version",
+        "data_release_version",
+        "dataset_sha256",
+        "model_wheel_sha256",
+        "core_wheel_sha256",
+    ],
+)
+def test_a_ledger_baseline_must_match_every_previous_certificate_pin(
+    tmp_path, changed_pin
+):
+    from policyengine.provenance import uk_candidate
+    from policyengine.provenance.qualification_ledger import write_json
+    from tests.qualification_fixtures import make_pair, make_receipt
+
+    pair = make_pair()
+    previous = {
+        "bundle_version": "6.3.0",
+        "qualification_pair_id": pair.pair_id,
+        "model_version": pair.model.version,
+        "core_version": pair.core.version,
+        "data_release_version": pair.data.release_version,
+        "dataset_sha256": pair.data.dataset_sha256,
+    }
+    write_json(
+        tmp_path / "uk" / pair.pair_id / "qualification_receipt.json",
+        make_receipt(pair).canonical_payload(),
+    )
+    previous[changed_pin] = "0" * 64 if changed_pin.endswith("sha256") else "999.0.0"
+    result, _ = uk_candidate.run_gate(
+        "metrics_review", lambda: uk_candidate._previous_pair_pins(previous, tmp_path)
+    )
+    assert result.status == "unavailable"
+    assert "different pairs" in result.summary
+
+
+def test_a_ledger_baseline_supplies_exact_wheel_and_data_pins(tmp_path):
+    from policyengine.provenance import uk_candidate
+    from policyengine.provenance.qualification_ledger import write_json
+    from tests.qualification_fixtures import make_pair, make_receipt
+
+    pair = make_pair()
+    previous = {
+        "bundle_version": "6.3.0",
+        "qualification_pair_id": pair.pair_id,
+        "model_version": pair.model.version,
+        "core_version": pair.core.version,
+        "data_release_version": pair.data.release_version,
+        "dataset_sha256": pair.data.dataset_sha256,
+    }
+    write_json(
+        tmp_path / "uk" / pair.pair_id / "qualification_receipt.json",
+        make_receipt(pair).canonical_payload(),
+    )
+    pins = uk_candidate._previous_pair_pins(previous, tmp_path)
+    assert pins["model_wheel_sha256"] == pair.model.wheel_sha256
+    assert pins["core_wheel_sha256"] == pair.core.wheel_sha256
+    assert pins["dataset_reference"] == {
+        "repo_id": pair.data.repo_id,
+        "repo_type": pair.data.repo_type,
+        "path": pair.data.dataset_path,
+        "revision": pair.data.dataset_revision,
+        "sha256": pair.data.dataset_sha256,
+    }
+
+
+@pytest.mark.parametrize("package", ["policyengine-uk", "policyengine-core"])
+@pytest.mark.parametrize(
+    "mismatch", ["version", "wheel", "installed bytes", "missing package"]
+)
+def test_baseline_identity_verification_detects_installed_mismatches(
+    tmp_path, monkeypatch, package, mismatch
+):
+    import json
+    import sys
+
+    from policyengine.provenance import uk_candidate, wheels
+
+    settings = {
+        "source": str(
+            __import__("pathlib").Path(uk_candidate.__file__).resolve().parents[1]
+        ),
+        "wheels": [
+            {
+                "name": name,
+                "version": "1.0.0",
+                "sha256": "1" * 64,
+                "path": str(tmp_path / f"{name}.whl"),
+            }
+            for name in ("policyengine-uk", "policyengine-core")
+        ],
+    }
+
+    def installed_version(name):
+        from importlib.metadata import PackageNotFoundError
+
+        if name == package and mismatch == "missing package":
+            raise PackageNotFoundError(name)
+        return "2.0.0" if name == package and mismatch == "version" else "1.0.0"
+
+    monkeypatch.setattr("importlib.metadata.version", installed_version)
+    monkeypatch.setattr(
+        wheels,
+        "verify_installed_against_wheel",
+        lambda name, path: {
+            "wheel_sha256": "2" * 64
+            if name == package and mismatch == "wheel"
+            else "1" * 64,
+            "problems": [f"{name}: mutated installed bytes"]
+            if name == package and mismatch == "installed bytes"
+            else [],
+        },
+    )
+    output = tmp_path / "identity.json"
+    monkeypatch.setattr(sys, "argv", ["runner", json.dumps(settings), str(output)])
+    # Restore the package namespace the isolated verifier creates when the test ends.
+    monkeypatch.setitem(sys.modules, "policyengine", sys.modules["policyengine"])
+    exec(
+        compile(uk_candidate.PREVIOUS_IDENTITY_RUNNER, "previous-identity", "exec"), {}
+    )
+    report = json.loads(output.read_text())
+    assert report["problems"] and package in report["problems"][0]
+
+
+def test_previous_data_is_downloaded_from_its_pin_and_verified(tmp_path, monkeypatch):
+    import hashlib
+    import json
+    from types import SimpleNamespace
+
+    import requests
+
+    from policyengine.provenance import uk_candidate
+
+    data = b"exact previous data"
+    digest = hashlib.sha256(data).hexdigest()
+    previous = _previous_certificate()
+    previous["dataset_sha256"] = previous["dataset_reference"]["sha256"] = digest
+    urls = []
+
+    def download(url, path, **kwargs):
+        urls.append(url)
+        path.parent.mkdir(parents=True)
+        path.write_bytes(data)
+        return path
+
+    monkeypatch.setattr(uk_candidate, "_download", download)
+    monkeypatch.setattr(
+        requests,
+        "get",
+        lambda *args, **kwargs: SimpleNamespace(
+            raise_for_status=lambda: None,
+            content=json.dumps(_previous_release_manifest(previous)).encode(),
+        ),
+    )
+    path = uk_candidate.previous_pair_dataset(previous, tmp_path)
+    assert urls == [
+        "https://huggingface.co/policyengine/uk-data/resolve/"
+        + "a" * 40
+        + "/national.h5"
+    ]
+    assert path.read_bytes() == data
+    path.write_bytes(b"wrong published wrapper data")
+    result, _ = uk_candidate.run_gate(
+        "metrics_review", lambda: uk_candidate.previous_pair_dataset(previous, tmp_path)
+    )
+    assert result.status == "unavailable"
+    assert "dataset bytes differ" in result.summary
+
+
+def _previous_release_manifest(previous):
+    return {
+        "schema_version": 1,
+        "country_id": "uk",
+        "data_package": {
+            "name": "uk-data",
+            "version": previous["data_release_version"],
+        },
+        "default_datasets": {"national": previous["dataset_name"]},
+        "artifacts": {
+            previous["dataset_name"]: {
+                "kind": "microdata",
+                **previous["dataset_reference"],
+            }
+        },
+    }
+
+
+@pytest.mark.parametrize(
+    "mismatch",
+    [
+        "release",
+        "default dataset",
+        "dataset sha",
+        "dataset revision",
+        "dataset path",
+        "dataset repo",
+        "dataset repo type",
+        "dataset absent",
+        "manifest sha",
+    ],
+)
+def test_previous_baseline_refuses_a_mismatched_release_manifest(
+    tmp_path, monkeypatch, mismatch
+):
+    import hashlib
+    import json
+    from types import SimpleNamespace
+
+    import requests
+
+    from policyengine.provenance import uk_candidate
+
+    previous = _previous_certificate()
+    data = b"certified previous data"
+    digest = hashlib.sha256(data).hexdigest()
+    previous["dataset_sha256"] = previous["dataset_reference"]["sha256"] = digest
+    path = tmp_path / "previous-data" / digest / "national.h5"
+    path.parent.mkdir(parents=True)
+    path.write_bytes(data)
+    manifest = _previous_release_manifest(previous)
+    if mismatch == "release":
+        manifest["data_package"]["version"] = "999.0.0"
+    elif mismatch == "default dataset":
+        manifest["default_datasets"]["national"] = "wrong"
+    elif mismatch == "dataset sha":
+        manifest["artifacts"]["national"]["sha256"] = "0" * 64
+    elif mismatch == "dataset revision":
+        manifest["artifacts"]["national"]["revision"] = "c" * 40
+    elif mismatch == "dataset path":
+        manifest["artifacts"]["national"]["path"] = "other.h5"
+    elif mismatch == "dataset repo":
+        manifest["artifacts"]["national"]["repo_id"] = "policyengine/other-data"
+    elif mismatch == "dataset repo type":
+        manifest["artifacts"]["national"]["repo_type"] = "dataset"
+    elif mismatch == "dataset absent":
+        manifest["artifacts"] = {}
+    else:
+        previous["release_manifest_reference"]["sha256"] = "0" * 64
+    monkeypatch.setattr(
+        requests,
+        "get",
+        lambda *a, **k: SimpleNamespace(
+            raise_for_status=lambda: None, content=json.dumps(manifest).encode()
+        ),
+    )
+    result, _ = uk_candidate.run_gate(
+        "metrics_review", lambda: uk_candidate.previous_pair_dataset(previous, tmp_path)
+    )
+    assert result.status == "unavailable"
+    assert "manifest" in result.summary
+
+
+@pytest.mark.parametrize(
+    "missing",
+    ["dataset_reference", "dataset_sha256", "core_version", "data_release_version"],
+)
+def test_unpinned_previous_baselines_are_incomplete(tmp_path, missing):
+    from policyengine.provenance import uk_candidate
+
+    previous = _previous_certificate()
+    del previous[missing]
+    result, _ = uk_candidate.run_gate(
+        "metrics_review", lambda: uk_candidate.previous_pair_dataset(previous, tmp_path)
+    )
+    assert result.status == "unavailable"
+
+
+def _orchestrate_metrics(tmp_path, monkeypatch, metrics, identity_mismatch=False):
+    """Run ``run_candidate_gates`` with only the metrics gate and ``metrics``
+    standing in for ``run_metrics_report``; ``(results, evidence)``."""
+
+    import json
+    from pathlib import Path
+    from types import SimpleNamespace
+
+    from policyengine.provenance import dataset_materialization, uk_candidate
+    from tests.qualification_fixtures import HARNESS, make_pair
+
+    previous = _previous_certificate()
+    previous_path = tmp_path / "certified-previous.h5"
+    candidate_path = tmp_path / "candidate.h5"
+    config = json.loads(
+        (
+            Path(__file__).parents[1] / "src/policyengine/data/qualification/uk.json"
+        ).read_text()
+    )
+    monkeypatch.setattr(uk_candidate, "_previous_pair_pins", lambda *args: previous)
+    monkeypatch.setattr(
+        uk_candidate, "previous_pair_python", lambda *args: "certified-previous-python"
+    )
+
+    def previous_dataset(*args):
+        if identity_mismatch:
+            raise uk_candidate.PreviousPairUnavailable(
+                "Previous dataset identity mismatch"
+            )
+        return previous_path
+
+    monkeypatch.setattr(uk_candidate, "previous_pair_dataset", previous_dataset)
+    monkeypatch.setattr(
+        dataset_materialization,
+        "materialize_dataset",
+        lambda *a, **k: SimpleNamespace(path=candidate_path),
+    )
+    monkeypatch.setattr(uk_candidate, "vendor_metrics_tool", lambda *a, **k: tmp_path)
+
+    def only_metrics(gates):
+        gate = next(function for name, function in gates if name == "metrics_review")
+        result, evidence = uk_candidate.run_gate("metrics_review", gate)
+        return [result], {"metrics_review": evidence}
+
+    monkeypatch.setattr(uk_candidate, "run_gates", only_metrics)
+    monkeypatch.setattr(uk_candidate, "run_metrics_report", metrics)
+    return uk_candidate.run_candidate_gates(
+        SimpleNamespace(pair=make_pair()),
+        HARNESS,
+        config=config,
+        work_dir=tmp_path,
+        previous_certificate=previous,
+        ledger_root=None,
+    )
+
+
+def test_a_malformed_metrics_report_is_not_written_to_the_ledger(tmp_path, monkeypatch):
+    """The attempt's evidence goes to a public ledger branch: a report that
+    breaks the small-cell format is kept only as its digest and problems."""
+
+    import json
+
+    marker = 987654321.5  # synthetic
+
+    def metrics(python, vendor, **settings):
+        report = _report()
+        report["dataset_path"] = str(settings["dataset_path"])
+        if settings["label"] == "candidate":
+            report["metrics"][0].update(value=marker, suppressed=True)
+        return report
+
+    results, evidence = _orchestrate_metrics(tmp_path, monkeypatch, metrics)
+    assert results[0].status == "fail"
+    assert set(evidence["metrics_report_candidate"]) == {
+        "withheld",
+        "report_sha256",
+        "problems",
+    }
+    assert str(marker) not in json.dumps(evidence["metrics_report_candidate"])
+    assert evidence["metrics_report_previous"]["metrics"] == _report()["metrics"]
+
+
+@pytest.mark.parametrize("identity_mismatch", [False, True])
+def test_metric_orchestration_uses_the_verified_previous_dataset_before_computing(
+    tmp_path, monkeypatch, identity_mismatch
+):
+    calls = []
+
+    def metrics(python, vendor, **settings):
+        calls.append((python, settings))
+        report = _report()
+        report["dataset_path"] = str(settings["dataset_path"])
+        if settings["label"] != "previous":
+            report["metrics"][0]["value"] = 1e12
+        return report
+
+    results, _ = _orchestrate_metrics(tmp_path, monkeypatch, metrics, identity_mismatch)
+    previous_path = tmp_path / "certified-previous.h5"
+    if identity_mismatch:
+        assert results[0].status == "unavailable"
+        assert calls == [], (
+            "Unverified baselines must be refused before computing metrics."
+        )
+    else:
+        assert results[0].status == "needs_review"
+        previous_call = next(call for call in calls if call[1]["label"] == "previous")
+        assert previous_call[0] == "certified-previous-python"
+        assert previous_call[1]["dataset_path"] == previous_path
+        assert "data_dir" not in previous_call[1]
+
+
+@pytest.mark.parametrize(
+    "failure", ["install", "verifier", "missing report", "bad json", "bad schema"]
+)
+def test_baseline_installation_or_verification_failure_is_incomplete(
+    tmp_path, monkeypatch, failure
+):
+    import subprocess
+    from pathlib import Path
+
+    from policyengine.provenance import uk_candidate
+
+    previous, _, fake_run = _previous_install_io(tmp_path, monkeypatch)
+
+    def run(command, **kwargs):
+        if (failure == "install" and command[:3] == ["uv", "pip", "install"]) or (
+            failure == "verifier" and "-c" in command
+        ):
+            raise subprocess.CalledProcessError(1, command)
+        fake_run(command, **kwargs)
+        if "-c" in command:
+            output = Path(command[-1])
+            if failure == "missing report":
+                output.unlink()
+            elif failure == "bad json":
+                output.write_text("invalid json")
+            elif failure == "bad schema":
+                output.write_text("[]")
+
+    result, _ = uk_candidate.run_gate(
+        "metrics_review",
+        lambda: uk_candidate.previous_pair_python(previous, tmp_path, run=run),
+    )
+    assert result.status == "unavailable"
+
+
+@pytest.mark.parametrize(
+    "mismatch", ["receipt pair", "receipt verdict", "dataset reference"]
+)
+def test_previous_pair_cannot_borrow_another_ledgers_identity(tmp_path, mismatch):
+    from policyengine.provenance import uk_candidate
+    from policyengine.provenance.qualification_ledger import write_json
+    from tests.qualification_fixtures import gates, make_pair, make_receipt
+
+    pair = make_pair()
+    previous = {
+        "bundle_version": "6.3.0",
+        "qualification_pair_id": pair.pair_id,
+        "model_version": pair.model.version,
+        "core_version": pair.core.version,
+        "data_release_version": pair.data.release_version,
+        "dataset_sha256": pair.data.dataset_sha256,
+    }
+    receipt = make_receipt(pair)
+    if mismatch == "receipt pair":
+        receipt = make_receipt(make_pair(core="3.32.0"))
+        previous["core_version"] = (
+            "3.32.0"  # Other pins match; the receipt's pair id must still match.
+        )
+    elif mismatch == "receipt verdict":
+        receipt = make_receipt(pair, gate_results=gates("fail"))
+    else:
+        previous["dataset_reference"] = {"path": "other.h5"}
+    write_json(
+        tmp_path / "uk" / pair.pair_id / "qualification_receipt.json",
+        receipt.canonical_payload(),
+    )
+    result, _ = uk_candidate.run_gate(
+        "metrics_review", lambda: uk_candidate._previous_pair_pins(previous, tmp_path)
+    )
+    assert result.status == "unavailable"
diff --git a/tests/test_uk_qualification_pipeline.py b/tests/test_uk_qualification_pipeline.py
new file mode 100644
index 00000000..f210e103
--- /dev/null
+++ b/tests/test_uk_qualification_pipeline.py
@@ -0,0 +1,1815 @@
+"""Orchestration of UK candidate qualification (no network).
+
+Invariants:
+
+- resolution keeps the certified core when the model accepts it and never
+  picks a prerelease, yanked or unpublished version;
+- the decision never qualifies a pair main already certifies or one main
+  supersedes, and never repeats a terminal attempt;
+- rewriting the certified-version test constants leaves no old value behind
+  and touches only quoted literals;
+- the replay request built for a candidate is valid against the scorecard's
+  vendored schema and binds the candidate commit, wheels and dataset;
+- promotion planning readies only the selected target and closes exactly the
+  candidates it or main supersede.
+"""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import os
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+from types import SimpleNamespace
+
+import jsonschema
+import pytest
+
+from policyengine.provenance.qualification import ReplayStatus
+from policyengine.provenance.qualification_ledger import write_json
+from policyengine.provenance.uk_qualification import (
+    accept_review,
+    assemble_receipt,
+    build_replay_request,
+    candidate_action,
+    candidate_branch,
+    certified_constant_replacements,
+    choose_core_version,
+    core_requirement,
+    latest_data_release,
+    plan_promotion,
+    previous_certificate,
+    read_replay,
+    resolve_candidate,
+    rewrite_certified_constants,
+)
+from tests.qualification_fixtures import (
+    HARNESS,
+    ISSUED,
+    VENDORED_SCHEMAS,
+    gates,
+    golden,
+    make_pair,
+    make_receipt,
+    sha,
+)
+
+REPO_ROOT = Path(__file__).resolve().parents[1]
+BUNDLE = json.loads(
+    (REPO_ROOT / "src/policyengine/data/bundle/manifest.json").read_text()
+)
+
+
+def _wheel(name, version, digest):
+    return {
+        "packagetype": "bdist_wheel",
+        "filename": f"{name.replace('-', '_')}-{version}-py3-none-any.whl",
+        "url": f"https://files.pythonhosted.org/packages/{name}-{version}.whl",
+        "digests": {"sha256": digest},
+    }
+
+
+MANIFEST = {
+    "schema_version": 1,
+    "data_package": {"name": "policyengine-uk-data", "version": "1.58.0"},
+    "default_datasets": {"national": "enhanced_frs_2024_25"},
+    "build": {"build_id": "policyengine-uk-data-1.58.0"},
+    "artifacts": {
+        "enhanced_frs_2024_25": {
+            "kind": "microdata",
+            "path": "enhanced_frs_2024_25.h5",
+            "repo_id": "policyengine/policyengine-uk-data-private",
+            "revision": "1.58.0",
+            "sha256": "d" * 64,
+            "size_bytes": 128369110,
+        }
+    },
+}
+
+
+def _resolver(**overrides):
+    projects = {
+        "policyengine-uk": {
+            "releases": {
+                "2.126.6": [_wheel("policyengine-uk", "2.126.6", "1" * 64)],
+                "2.126.8": [_wheel("policyengine-uk", "2.126.8", "2" * 64)],
+                "2.127.0rc1": [_wheel("policyengine-uk", "2.127.0rc1", "3" * 64)],
+                "2.127.1": [
+                    {**_wheel("policyengine-uk", "2.127.1", "4" * 64), "yanked": True}
+                ],
+            }
+        },
+        "policyengine-uk/2.126.8": {
+            "info": {"requires_dist": ["policyengine-core>=3.32.27"]}
+        },
+        "policyengine-uk/2.126.6": {
+            "info": {"requires_dist": ["policyengine-core>=3.34.0"]}
+        },
+        "policyengine-core": {
+            "releases": {
+                v: [_wheel("policyengine-core", v, v.replace(".", "")[0] * 64)]
+                for v in ("3.33.0", "3.34.0", "3.34.1", "3.35.0rc1")
+            }
+        },
+    }
+    manifest_bytes = json.dumps(MANIFEST).encode()
+    kwargs = dict(
+        current_core="3.33.0",
+        pypi_project=lambda path: projects[path],
+        hf_revision_sha=lambda repo, rev: "f" * 40,
+        hf_tags=lambda repo: ["1.57.4", "1.58.0", "v1.11.1", "version_a"],
+        hf_file=lambda repo, rev, path: manifest_bytes,
+    )
+    kwargs.update(overrides)
+    return resolve_candidate(**kwargs), manifest_bytes
+
+
+def test_resolution_picks_newest_stable_and_keeps_an_accepted_core():
+    resolved, manifest_bytes = _resolver()
+    pair = resolved.pair
+    assert pair.model.version == "2.126.8"
+    assert pair.model.wheel_sha256 == "2" * 64
+    assert pair.core.version == "3.33.0"
+    assert pair.data.release_version == "1.58.0"
+    assert pair.data.release_manifest_revision == "f" * 40
+    assert (
+        pair.data.release_manifest_sha256 == hashlib.sha256(manifest_bytes).hexdigest()
+    )
+    assert resolved.manifest_uri.startswith(
+        "hf://model/policyengine/policyengine-uk-data-private@"
+    )
+
+
+def test_resolution_moves_core_only_when_the_model_requires_it():
+    resolved, _ = _resolver(model_version="2.126.6")
+    assert resolved.pair.core.version == "3.34.1"
+
+
+def test_core_requirement_and_choice():
+    assert (
+        core_requirement(["numpy>=1", "policyengine_core>=3.32.27,<4"])
+        == "<4,>=3.32.27"
+    )
+    with pytest.raises(ValueError):
+        core_requirement(["numpy"])
+    with pytest.raises(ValueError):
+        choose_core_version("3.33.0", ">=9", [])
+
+
+def test_latest_data_release_ignores_non_version_tags():
+    assert latest_data_release(["1.57.4", "1.58.0", "v1.11.1", "version_a"]) == "1.58.0"
+
+
+def test_resolution_refuses_unpublished_versions():
+    with pytest.raises(ValueError):
+        _resolver(model_version="9.9.9")
+
+
+def test_actions(tmp_path):
+    current = BUNDLE["data_releases"]["uk"]
+    certified = make_pair(
+        model=current["model_package"]["version"],
+        data=current["data_package"]["version"],
+        core=BUNDLE["packages"]["policyengine-core"]["version"],
+        dataset_sha=current["certified_data_artifact"]["sha256"],
+    )
+    assert (
+        candidate_action(certified, BUNDLE, tmp_path, base_commit="a" * 40)[0]
+        == "already_certified"
+    )
+    older = make_pair(model="2.100.0", data="1.50.0", core="3.30.0")
+    assert candidate_action(older, BUNDLE, tmp_path, base_commit="a" * 40)[0] == "stale"
+    newer = make_pair(model="2.200.0", data="1.58.0", core="3.33.0")
+    assert (
+        candidate_action(newer, BUNDLE, tmp_path, base_commit="a" * 40)[0] == "qualify"
+    )
+    rejected = make_receipt(pair=newer, gate_results=gates(wheel_hashes="fail"))
+    write_json(
+        tmp_path / "uk" / newer.pair_id / "qualification_receipt.json",
+        rejected.canonical_payload(),
+    )
+    assert (
+        candidate_action(newer, BUNDLE, tmp_path, base_commit="a" * 40)[0]
+        == "duplicate"
+    )
+    assert (
+        candidate_action(newer, BUNDLE, tmp_path, base_commit="a" * 40, force=True)[0]
+        == "qualify"
+    )
+
+
+def test_incomplete_pairs_requalify_on_a_new_base_only(tmp_path):
+    pair = make_pair(model="2.200.0")
+    incomplete = make_receipt(
+        pair=pair, gate_results=gates(reform_contract="unavailable")
+    )
+    write_json(
+        tmp_path / "uk" / pair.pair_id / "qualification_receipt.json",
+        incomplete.canonical_payload(),
+    )
+    (tmp_path / "uk" / pair.pair_id / "attempts" / ("a" * 12)).mkdir(parents=True)
+    assert (
+        candidate_action(pair, BUNDLE, tmp_path, base_commit="a" * 40)[0] == "duplicate"
+    )
+    assert (
+        candidate_action(pair, BUNDLE, tmp_path, base_commit="b" * 40)[0] == "qualify"
+    )
+
+
+def _new_record(old):
+    new = json.loads(json.dumps(old))
+    new["model_package"]["version"] = "2.126.8"
+    new["certification"]["built_with_model_version"] = "2.126.0"
+    new["certification"]["data_build_id"] = "policyengine-uk-data-1.59.0"
+    new["data_package"]["version"] = "1.59.0"
+    new["data_package"]["release_manifest_revision"] = "1" * 40
+    return new
+
+
+def test_constant_rewrite_leaves_no_old_values(tmp_path):
+    for relative in (
+        "tests/test_models.py",
+        "tests/test_release_manifests.py",
+        "tests/test_uk_regions.py",
+    ):
+        (tmp_path / relative).parent.mkdir(parents=True, exist_ok=True)
+        shutil.copy(REPO_ROOT / relative, tmp_path / relative)
+    old = BUNDLE["data_releases"]["uk"]
+    new = _new_record(old)
+    replacements = certified_constant_replacements(old, new)
+    changed = rewrite_certified_constants(tmp_path, replacements)
+    assert set(changed) == {
+        "tests/test_models.py",
+        "tests/test_release_manifests.py",
+        "tests/test_uk_regions.py",
+    }
+    for relative in changed:
+        text = (tmp_path / relative).read_text()
+        for before, after in replacements:
+            assert f'"{before}"' not in text and f'@{before}"' not in text
+    manifests = (tmp_path / "tests/test_release_manifests.py").read_text()
+    assert 'UK_MODEL_VERSION = "2.126.8"' in manifests
+    assert 'UK_DATA_RELEASE_PATH = "releases/1.59.0/release_manifest.json"' in manifests
+    assert 'UK_DATA_RELEASE_REVISION = "' + "1" * 40 + '"' in manifests
+    assert rewrite_certified_constants(tmp_path, replacements) == []
+
+
+@pytest.mark.parametrize("qualification_only", [False, True])
+def test_preparation_generates_new_sidecars_and_verifies_the_exact_pair(
+    tmp_path, monkeypatch, qualification_only
+):
+    """Run preparation, certification and real TRO generation without a network."""
+
+    from policyengine.provenance import certification, manifest
+    from policyengine.provenance.uk_qualification import prepare_candidate_commit
+
+    monkeypatch.syspath_prepend(str(REPO_ROOT / "scripts"))
+    import bundle as maintenance
+    import certify_data_release as certify_script
+    import generate_bundle_artifacts as artifacts
+    import generate_trace_tros as tros
+
+    release = json.loads(json.dumps(MANIFEST))
+    release["build"].update(
+        built_with_model_package={
+            "name": "policyengine-uk",
+            "version": "2.100.0" if qualification_only else "2.126.8",
+        },
+        built_with_core_package={
+            "name": "policyengine-core",
+            "version": "3.0.0" if qualification_only else "3.33.0",
+        },
+    )
+    release_bytes = json.dumps(release).encode()
+    resolved, _ = _resolver(hf_file=lambda *args: release_bytes)
+    pair = resolved.pair
+    bundle_dir = tmp_path / "src/policyengine/data/bundle"
+    bundle_dir.mkdir(parents=True)
+    source = json.loads(json.dumps(BUNDLE))
+    source["data_releases"].pop("us")
+    source["packages"].pop("policyengine-us")
+    for packages in source["extras"].values():
+        if "policyengine-us" in packages:
+            packages.remove("policyengine-us")
+    bundle_path = bundle_dir / "manifest.json"
+    write_json(bundle_path, source)
+    sidecar = bundle_dir / "uk.trace.tro.jsonld"
+    old_sidecar = (
+        REPO_ROOT / "src/policyengine/data/bundle/uk.trace.tro.jsonld"
+    ).read_bytes()
+    sidecar.write_bytes(old_sidecar)
+    shutil.copy(REPO_ROOT / "pyproject.toml", tmp_path / "pyproject.toml")
+    (tmp_path / "changelog.d").mkdir()
+    for name in ("test_models.py", "test_release_manifests.py", "test_uk_regions.py"):
+        (tmp_path / "tests").mkdir(exist_ok=True)
+        shutil.copy(REPO_ROOT / "tests" / name, tmp_path / "tests" / name)
+
+    def country_manifest(country):
+        assert country == "uk"
+        result = manifest.CountryReleaseManifest.model_validate(
+            json.loads(bundle_path.read_text())["data_releases"][country]
+        )
+        result.source_sha256 = hashlib.sha256(bundle_path.read_bytes()).hexdigest()
+        return result
+
+    def data_manifest(country=None):
+        result = manifest.DataReleaseManifest.model_validate_json(release_bytes)
+        result.source_sha256 = hashlib.sha256(release_bytes).hexdigest()
+        return result
+
+    monkeypatch.setattr(certify_script, "REPO_ROOT", tmp_path)
+    monkeypatch.setattr(maintenance, "REPO_ROOT", tmp_path)
+    monkeypatch.setattr(artifacts, "REPO_ROOT", tmp_path)
+    monkeypatch.setattr(artifacts, "BUNDLE_MANIFEST", bundle_path)
+    monkeypatch.setattr(artifacts, "PYPROJECT", tmp_path / "pyproject.toml")
+    load_bundle = artifacts.load_bundle_manifest
+    monkeypatch.setattr(
+        artifacts, "load_bundle_manifest", lambda path=bundle_path: load_bundle(path)
+    )
+    monkeypatch.setattr(tros, "BUNDLE_MANIFEST", bundle_path)
+    monkeypatch.setattr(tros, "BUNDLE_TRO_DIR", bundle_dir)
+    monkeypatch.setattr(tros, "BUNDLE_LOCK", tmp_path / "uv.lock")
+    monkeypatch.setattr(tros, "get_release_manifest", country_manifest)
+    monkeypatch.setattr(tros, "get_data_release_manifest", data_manifest)
+    monkeypatch.setattr(
+        certification,
+        "fetch_release_manifest",
+        lambda uri, **kw: (
+            data_manifest(),
+            pair.data.release_manifest_sha256,
+            certification.parse_manifest_uri(uri),
+        ),
+    )
+    monkeypatch.setattr(
+        certification,
+        "fetch_pypi_wheel_metadata",
+        lambda *args: {
+            "sha256": pair.model.wheel_sha256,
+            "url": resolved.model_wheel.url,
+        },
+    )
+    monkeypatch.setattr(certification, "head_artifact", lambda *args, **kw: True)
+    monkeypatch.setattr(
+        certification, "required_supplemental_release_files", lambda *args: []
+    )
+    calls = []
+
+    def run(command, *, cwd, env=None):
+        calls.append(command)
+        assert cwd == tmp_path
+        if command[1] == "scripts/bundle.py":
+            assert maintenance.main(command[2:]) == 0
+        elif command[:2] == ["uv", "lock"]:
+            (tmp_path / "uv.lock").write_text(
+                "[[package]]\n"
+                'name = "policyengine-uk"\n'
+                f'version = "{pair.model.version}"\n'
+                'source = { registry = "https://pypi.org/simple" }\n'
+                f'wheels = [{{url = "{resolved.model_wheel.url}", hash = "sha256:{pair.model.wheel_sha256}"}}]\n'
+            )
+        else:
+            assert command[1] == "scripts/release_lock.py"
+        return subprocess.CompletedProcess(command, 0)
+
+    prepared = prepare_candidate_commit(
+        tmp_path, resolved, python=sys.executable, run=run, refresh_snapshots=False
+    )
+    assert sidecar.read_bytes() != old_sidecar
+    assert pair.data.release_manifest_sha256.encode() in sidecar.read_bytes()
+    assert prepared["compatibility_basis"] == (
+        "qualified_exact_pair" if qualification_only else "built_with_model_package"
+    )
+    assert (tmp_path / prepared["changelog_fragment"]).is_file()
+    assert maintenance.main(["check", "--include-tros", "--strict-tros"]) == 0
+    assert calls[-1][2:] == ["check", "--include-tros", "--strict-tros"]
+    if qualification_only:
+        changed = json.loads(bundle_path.read_bytes())
+        changed["packages"]["policyengine-core"]["version"] = "99.0.0"
+        write_json(bundle_path, changed)
+        with pytest.raises(ValueError, match="runtime core.*qualified exact pair"):
+            tros.generated_tros(strict=True)
+
+
+def test_replay_request_is_valid_for_the_scorecard():
+    pair = make_pair()
+    release_bundle = dict(
+        golden("qualification_request_golden")["candidate"]["expected_release_bundle"]
+    )
+    request = build_replay_request(
+        pair, HARNESS, release_bundle=release_bundle, issued_by=ISSUED
+    )
+    schema = json.loads(
+        (VENDORED_SCHEMAS / "qualification_request.v1.json").read_text()
+    )
+    jsonschema.Draft202012Validator(schema).validate(request.document())
+    candidate = request.document()["candidate"]
+    assert candidate["policyengine"]["source"] == {
+        "git": {
+            "repository": "PolicyEngine/policyengine.py",
+            "commit": HARNESS.git_commit,
+        }
+    }
+    assert candidate["policyengine_uk"]["wheel_sha256"] == pair.model.wheel_sha256
+    assert candidate["data"]["sha256"] == pair.data.dataset_sha256
+    assert request.document()["requested_by"]["pair_id"] == pair.pair_id
+    again = build_replay_request(
+        pair, HARNESS, release_bundle=release_bundle, issued_by=ISSUED
+    )
+    assert again.request_id == request.request_id
+
+
+def test_the_real_cli_generates_replay_requests_despite_country_import_suppression(
+    tmp_path,
+):
+    """The real CLI can read the UK certificate despite automatic import suppression."""
+
+    from importlib.util import find_spec
+
+    if find_spec("policyengine_uk") is None:
+        pytest.skip("Replay request generation requires the installed UK model")
+    attempt = tmp_path / "attempt"
+    write_json(attempt / "candidate.json", {"pair": make_pair().canonical_payload()})
+    write_json(attempt / "harness.json", HARNESS.canonical_payload())
+    env = dict(os.environ)
+    env["POLICYENGINE_SKIP_COUNTRY_IMPORTS"] = "1"
+    env.pop("GITHUB_OUTPUT", None)
+    result = subprocess.run(
+        [
+            sys.executable,
+            str(REPO_ROOT / "scripts/check_uk_candidate.py"),
+            "request-replay",
+            "--attempt",
+            str(attempt),
+        ],
+        cwd=REPO_ROOT,
+        env=env,
+        capture_output=True,
+        text=True,
+        timeout=600,
+    )
+    assert result.returncode == 0, result.stdout + result.stderr
+    request = json.loads((attempt / "replay/request.json").read_text())
+    schema = json.loads(
+        (VENDORED_SCHEMAS / "qualification_request.v1.json").read_text()
+    )
+    jsonschema.Draft202012Validator(schema).validate(request)
+    candidate = request["candidate"]
+    assert candidate["policyengine"]["source"]["git"]["commit"] == HARNESS.git_commit
+    bundle = candidate["expected_release_bundle"]
+    record = BUNDLE["data_releases"]["uk"]
+    assert bundle["model_version"] == record["model_package"]["version"]
+    assert bundle["data_version"] == record["data_package"]["version"]
+    assert bundle["default_dataset"] == record["default_dataset"]
+
+
+def test_reading_replay_state():
+    pair = make_pair()
+    request = build_replay_request(
+        pair,
+        HARNESS,
+        release_bundle=golden("qualification_request_golden")["candidate"][
+            "expected_release_bundle"
+        ],
+        issued_by=ISSUED,
+    )
+    gate, status, receipt = read_replay(
+        request, fetch=lambda ref, path: None, state_ref="replay-state"
+    )
+    assert gate.status == "unavailable" and status is None
+    running = ReplayStatus.model_validate(
+        {
+            "schema": "policyengine.uk-replay-status/v1",
+            "request_id": request.request_id,
+            "status": "running",
+            "history": [
+                {
+                    "status": "running",
+                    "at": "2026-10-15T10:00:00Z",
+                    "run_url": "https://github.com/PolicyEngine/policyengine-scorecard/actions/runs/3",
+                }
+            ],
+        }
+    ).document()
+    gate, status, _ = read_replay(
+        request,
+        fetch=lambda ref, path: (
+            json.dumps(running).encode() if path.endswith("STATUS.json") else None
+        ),
+        state_ref="replay-state",
+    )
+    assert gate.status == "unavailable" and status.status == "running"
+    terminal = {
+        **running,
+        "status": "complete",
+        "receipt_sha256": "a" * 64,
+        "receipt_commit": "c" * 40,
+    }
+    gate, _, _ = read_replay(
+        request,
+        fetch=lambda ref, path: (
+            json.dumps(terminal).encode() if path.endswith("STATUS.json") else None
+        ),
+        state_ref="replay-state",
+    )
+    assert gate.status == "fail" and "no REPLAY_RECEIPT.json" in gate.summary
+
+
+def test_assembled_receipt_without_replay_is_incomplete():
+    pair = make_pair()
+    local = [gate for gate in gates() if gate.gate_id != "replay"]
+    receipt = assemble_receipt(
+        pair,
+        HARNESS,
+        local,
+        issued_by=ISSUED,
+        completed_at="2026-10-15T10:00:00Z",
+        previous=None,
+        replay=None,
+    )
+    assert receipt.verdict == "incomplete"
+    assert [gate.gate_id for gate in receipt.gates] == [gate.gate_id for gate in local]
+
+
+def test_acceptance_is_bound_to_the_receipt():
+    receipt = make_receipt(gate_results=gates(metrics_review="needs_review"))
+    with pytest.raises(ValueError):
+        accept_review(
+            receipt,
+            reviewer="maria",
+            accepted_receipt_sha256=sha("1"),
+            explanation="x",
+            accepted_at="t",
+        )
+    reviewed = accept_review(
+        receipt,
+        reviewer="maria",
+        accepted_receipt_sha256=receipt.unreviewed_sha256,
+        explanation="Council tax move follows the 2025-26 band revaluation in uk-data.",
+        accepted_at="2026-10-16T09:00:00Z",
+    )
+    assert reviewed.promotable
+    with pytest.raises(ValueError):
+        accept_review(
+            make_receipt(),
+            reviewer="m",
+            accepted_receipt_sha256=sha("1"),
+            explanation="x",
+            accepted_at="t",
+        )
+
+
+def _promotion_receipt(**kwargs):
+    return make_receipt(**kwargs).model_copy(
+        update={"previous_certificate": previous_certificate(BUNDLE)}
+    )
+
+
+def test_promotion_plan():
+    current = (
+        BUNDLE["data_releases"]["uk"]["model_package"]["version"],
+        BUNDLE["data_releases"]["uk"]["data_package"]["version"],
+        BUNDLE["packages"]["policyengine-core"]["version"],
+    )
+    newest = _promotion_receipt(
+        pair=make_pair(model="2.200.0", data=current[1], core=current[2])
+    )
+    older = _promotion_receipt(
+        pair=make_pair(model="2.199.0", data=current[1], core=current[2])
+    )
+    rejected = _promotion_receipt(
+        pair=make_pair(model="2.201.0", data=current[1], core=current[2]),
+        gate_results=gates(replay="fail"),
+    )
+    stale = _promotion_receipt(
+        pair=make_pair(model="2.0.0", data="1.0.0", core="3.0.0")
+    )
+    branches = {
+        candidate_branch(r.pair): r.harness.git_commit
+        for r in (newest, older, rejected, stale)
+    }
+    branches["candidate/uk/unknown"] = "0" * 40
+    plan = plan_promotion([newest, older, rejected, stale], BUNDLE, branches)
+    assert plan.target == newest
+    assert plan.ready_branch == candidate_branch(newest.pair)
+    assert plan.ready_commit == newest.harness.git_commit
+    assert plan.close_branches == sorted(
+        [candidate_branch(older.pair), candidate_branch(stale.pair)]
+    )
+    assert candidate_branch(rejected.pair) not in plan.close_branches
+
+
+def _promotable_pair_and_branch(core=None):
+    current = BUNDLE["data_releases"]["uk"]["data_package"]["version"]
+    core = core or BUNDLE["packages"]["policyengine-core"]["version"]
+    return make_pair(model="2.200.0", data=current, core=core)
+
+
+def test_pairs_that_share_versions_never_share_a_branch():
+    """Interrupted review of #574: a qualified pair readied the branch of an
+    incomplete pair with the same model and data but another core."""
+
+    qualified_pair = _promotable_pair_and_branch()
+    other_core = _promotable_pair_and_branch(core="3.99.0")
+    assert qualified_pair.pair_id != other_core.pair_id
+    assert candidate_branch(qualified_pair) != candidate_branch(other_core)
+    assert qualified_pair.pair_id[:12] in candidate_branch(qualified_pair)
+    qualified = _promotion_receipt(pair=qualified_pair)
+    incomplete = make_receipt(pair=other_core, gate_results=gates(replay="unavailable"))
+    plan = plan_promotion(
+        [qualified, incomplete],
+        BUNDLE,
+        {candidate_branch(other_core): incomplete.harness.git_commit},
+    )
+    assert plan.target == qualified
+    assert plan.ready_branch is None
+    assert "has no open pull request" in plan.reason
+
+
+def test_a_branch_that_moved_off_the_qualified_commit_is_not_promoted():
+    """The replayed and tested commit must be the promoted commit."""
+
+    qualified = _promotion_receipt(pair=_promotable_pair_and_branch())
+    branch = candidate_branch(qualified.pair)
+    plan = plan_promotion([qualified], BUNDLE, {branch: "f" * 40})
+    assert plan.target == qualified and plan.ready_branch is None
+    assert "not the qualified commit" in plan.reason
+    plan = plan_promotion([qualified], BUNDLE, {branch: qualified.harness.git_commit})
+    assert plan.ready_branch == branch
+
+
+def test_promote_readies_and_merges_only_the_qualified_commit(monkeypatch, tmp_path):
+    import argparse
+    import importlib.util
+
+    qualified = _promotion_receipt(pair=_promotable_pair_and_branch())
+    branch = candidate_branch(qualified.pair)
+    spec = importlib.util.spec_from_file_location(
+        "check_uk_candidate_for_test",
+        Path(__file__).resolve().parents[1] / "scripts" / "check_uk_candidate.py",
+    )
+    cli = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(cli)
+    calls = []
+    monkeypatch.setattr(cli, "latest_receipts", lambda root, **_: [qualified])
+    monkeypatch.setattr(
+        cli, "subprocess", SimpleNamespace(run=lambda argv, **k: calls.append(argv))
+    )
+    monkeypatch.setattr(cli, "config", lambda: {"promotion": {"automatic": True}})
+    monkeypatch.setattr(cli, "_open_candidate_branches", lambda: {branch: "f" * 40})
+    assert cli.cmd_promote(argparse.Namespace(ledger=str(tmp_path), apply=True)) == 0
+    assert calls == []
+    monkeypatch.setattr(
+        cli,
+        "_open_candidate_branches",
+        lambda: {branch: qualified.harness.git_commit},
+    )
+    assert cli.cmd_promote(argparse.Namespace(ledger=str(tmp_path), apply=True)) == 0
+    assert calls[0][:3] == ["gh", "pr", "ready"] and calls[0][3] == branch
+    assert calls[1][-2:] == ["--match-head-commit", qualified.harness.git_commit]
+
+
+def test_promotion_requires_the_current_supported_years_baseline(monkeypatch):
+    """C passed against A, but loses the year B added before C finished."""
+
+    from policyengine.provenance.certification import (
+        BASIS_QUALIFIED_PAIR,
+        build_bundle_data_release_payload,
+        build_country_manifest_payload,
+    )
+    from policyengine.provenance.manifest import (
+        CountryReleaseManifest,
+        DataReleaseManifest,
+    )
+    from policyengine.provenance.uk_candidate import supported_years
+
+    core = BUNDLE["packages"]["policyengine-core"]["version"]
+    pair_b = make_pair(model="2.131.0", data="1.58.2", core=core)
+    pair_c = make_pair(model="2.132.0", data="1.58.3", core=core)
+    years_a = list(range(2025, 2032))
+    years_b = list(range(2025, 2033))
+    monkeypatch.setattr(
+        "policyengine.provenance.uk_candidate.data_year", lambda _: 2024
+    )
+
+    def gate(pair, years, previous_years):
+        return supported_years(
+            pair,
+            Path("projected.h5"),
+            required_years=years_a[:-1],
+            budget_horizon_years=years_a[1:],
+            previous_supported_years=previous_years,
+            projected_years=lambda _: years,
+        )[0]
+
+    gate_b = gate(pair_b, years_b, years_a)
+    gate_c = gate(pair_c, years_a, years_a)
+    assert gate_b.status == gate_c.status == "pass"
+    assert gate(pair_c, years_a, years_b).status == "fail"
+    receipt_b = _promotion_receipt(
+        pair=pair_b,
+        gate_results=[gate_b if g.gate_id == "supported_years" else g for g in gates()],
+    )
+    receipt_c = _promotion_receipt(
+        pair=pair_c,
+        gate_results=[gate_c if g.gate_id == "supported_years" else g for g in gates()],
+    )
+    assert receipt_b.promotable and receipt_c.promotable
+
+    release = json.loads(json.dumps(MANIFEST))
+    release["data_package"]["version"] = pair_b.data.release_version
+    release["build"]["build_id"] = pair_b.data.build_id
+    release["artifacts"][pair_b.data.dataset]["revision"] = pair_b.data.dataset_revision
+    manifest = DataReleaseManifest.model_validate(release)
+    uri_parts = {
+        "repo_id": pair_b.data.repo_id,
+        "repo_type": pair_b.data.repo_type,
+        "path": pair_b.data.release_manifest_path,
+        "revision": pair_b.data.release_manifest_revision,
+    }
+    country = build_country_manifest_payload(
+        country="uk",
+        manifest=manifest,
+        uri_parts=uri_parts,
+        policyengine_version=BUNDLE["policyengine_version"],
+        model_package=pair_b.model.name,
+        model_version=pair_b.model.version,
+        model_wheel={"sha256": pair_b.model.wheel_sha256},
+        compatibility_basis=BASIS_QUALIFIED_PAIR,
+        core_version=core,
+        qualified_pair_id=pair_b.pair_id,
+    )
+    record_b = build_bundle_data_release_payload(
+        country_payload=country,
+        data_producer="populace",
+        manifest_uri=f"hf://model/{pair_b.data.repo_id}@{pair_b.data.release_manifest_revision}/{pair_b.data.release_manifest_path}",
+        uri_parts=uri_parts,
+    )
+    CountryReleaseManifest.model_validate(record_b)
+    current_b = json.loads(json.dumps(BUNDLE))
+    current_b["data_releases"]["uk"] = record_b
+    branches = {candidate_branch(pair_c): receipt_c.harness.git_commit}
+
+    assert plan_promotion([receipt_b, receipt_c], BUNDLE, branches).ready_branch
+    plan = plan_promotion([receipt_b, receipt_c], current_b, branches)
+    assert plan.target == receipt_c
+    assert plan.ready_branch is None and plan.ready_commit is None
+    assert "requalif" in plan.reason and "--force" in plan.reason
+
+
+@pytest.mark.parametrize(
+    "field",
+    [None, *type(previous_certificate(BUNDLE)).model_fields],
+)
+def test_promotion_binds_every_previous_certificate_field(field):
+    qualified = _promotion_receipt(pair=_promotable_pair_and_branch())
+    previous = qualified.previous_certificate
+    if field is None:
+        previous = None
+    else:
+        value = getattr(previous, field)
+        if field in {"dataset_reference", "release_manifest_reference"}:
+            value = value.model_copy(update={"revision": "0" * 40})
+        elif field.endswith("sha256") or field == "qualification_pair_id":
+            value = "0" * 64
+        else:
+            value = "different"
+        previous = previous.model_copy(update={field: value})
+    stale = qualified.model_copy(update={"previous_certificate": previous})
+    plan = plan_promotion(
+        [stale], BUNDLE, {candidate_branch(stale.pair): stale.harness.git_commit}
+    )
+    assert plan.target == stale
+    assert plan.ready_branch is None and plan.ready_commit is None
+    assert "requalif" in plan.reason
+
+
+@pytest.mark.parametrize("missing", [False, True])
+def test_stale_baseline_does_not_close_or_fall_back_to_other_candidates(missing):
+    previous = previous_certificate(BUNDLE)
+    newest = _promotion_receipt(pair=_promotable_pair_and_branch()).model_copy(
+        update={
+            "previous_certificate": None
+            if missing
+            else previous.model_copy(update={"bundle_version": "different"})
+        }
+    )
+    older = _promotion_receipt(
+        pair=make_pair(
+            model="2.199.0",
+            data=newest.pair.data.release_version,
+            core=newest.pair.core.version,
+        )
+    )
+    superseded_by_main = _promotion_receipt(
+        pair=make_pair(model="2.0.0", data="1.0.0", core="3.0.0")
+    )
+    receipts = [newest, older, superseded_by_main]
+    branches = {candidate_branch(r.pair): r.harness.git_commit for r in receipts}
+    plan = plan_promotion(receipts, BUNDLE, branches)
+    assert plan.target == newest
+    assert plan.ready_branch is None and plan.ready_commit is None
+    assert plan.close_branches == [candidate_branch(superseded_by_main.pair)]
+
+
+def test_promotion_on_an_unrelated_main_commit_keeps_the_same_baseline():
+    qualified = _promotion_receipt(pair=_promotable_pair_and_branch())
+    unrelated_main = json.loads(json.dumps(BUNDLE))
+    unrelated_main["data_releases"]["us"]["data_package"]["version"] = "99.0.0"
+    assert previous_certificate(unrelated_main) == qualified.previous_certificate
+    branch = candidate_branch(qualified.pair)
+    plan = plan_promotion(
+        [qualified], unrelated_main, {branch: qualified.harness.git_commit}
+    )
+    assert plan.ready_branch == branch
+    assert plan.ready_commit == qualified.harness.git_commit
+
+
+def test_a_forced_rerun_stages_a_new_attempt(tmp_path):
+    """Interrupted review of #574: a forced run reused the existing attempt key,
+    so the workflow's duplicate guard stopped it."""
+
+    from policyengine.provenance.qualification_ledger import attempt_directory
+    from policyengine.provenance.uk_qualification import attempt_key
+
+    pair = _promotable_pair_and_branch()
+    base = "a" * 40
+    assert attempt_key(pair, tmp_path, base_commit=base, force=True) == base[:12]
+    attempt_directory(tmp_path, "uk", pair.pair_id, base[:12]).mkdir(parents=True)
+    assert attempt_key(pair, tmp_path, base_commit=base) == base[:12]
+    assert attempt_key(pair, tmp_path, base_commit=base, force=True) == f"{base[:12]}-2"
+    attempt_directory(tmp_path, "uk", pair.pair_id, f"{base[:12]}-2").mkdir(
+        parents=True
+    )
+    assert attempt_key(pair, tmp_path, base_commit=base, force=True) == f"{base[:12]}-3"
+
+
+@pytest.mark.parametrize(
+    ("pr_state", "reset_fails"),
+    [("ready", False), ("draft", False), ("absent", False), ("ready", True)],
+)
+def test_the_workflow_drafts_existing_prs_before_replacing_their_head(
+    tmp_path, pr_state, reset_fails
+):
+    """Execute the workflow shell with local gh/git stand-ins and no remote writes."""
+
+    import yaml
+
+    workflow = yaml.safe_load(
+        (REPO_ROOT / ".github/workflows/certify-uk-release.yaml").read_text()
+    )
+    step = next(
+        step
+        for step in workflow["jobs"]["prepare"]["steps"]
+        if step.get("name") == "Commit and push the candidate commit"
+    )
+    state_path = tmp_path / "pr-state.json"
+    state_path.write_text(json.dumps({"status": pr_state}))
+    log_path = tmp_path / "commands.jsonl"
+    fake = f"""#!{sys.executable}
+import json
+import os
+import sys
+from pathlib import Path
+
+command = Path(sys.argv[0]).name
+args = sys.argv[1:]
+state_path = Path(os.environ["PR_STATE"])
+state = json.loads(state_path.read_text())
+with Path(os.environ["COMMAND_LOG"]).open("a") as log:
+    log.write(json.dumps([command, *args]) + "\\n")
+if command == "gh" and args[:2] == ["pr", "list"]:
+    if state["status"] == "ready":
+        print("574")
+elif command == "gh" and args[:2] == ["pr", "ready"]:
+    assert args[2:] == ["574", "--undo"]
+    if os.environ["RESET_FAILS"] == "1":
+        sys.exit(42)
+    state["status"] = "draft"
+    state_path.write_text(json.dumps(state))
+elif command == "git" and args[0] == "push":
+    if state["status"] == "ready":
+        sys.exit(91)
+"""
+    binary_dir = tmp_path / "bin"
+    binary_dir.mkdir()
+    for command in ("gh", "git"):
+        executable = binary_dir / command
+        executable.write_text(fake)
+        executable.chmod(0o755)
+    result = subprocess.run(
+        ["bash", "-e", "-o", "pipefail", "-c", step["run"]],
+        cwd=tmp_path,
+        env={
+            **os.environ,
+            "PATH": f"{binary_dir}{os.pathsep}{os.environ['PATH']}",
+            "BRANCH": "candidate/uk/2.126.8-1.58.0",
+            "PAIR_ID": "a" * 64,
+            "GH_TOKEN": "local-workflow-test",
+            "PR_STATE": str(state_path),
+            "COMMAND_LOG": str(log_path),
+            "RESET_FAILS": "1" if reset_fails else "0",
+        },
+        capture_output=True,
+        text=True,
+        timeout=30,
+    )
+    commands = [json.loads(line) for line in log_path.read_text().splitlines()]
+    pushes = [i for i, command in enumerate(commands) if command[:2] == ["git", "push"]]
+    resets = [
+        i for i, command in enumerate(commands) if command[:3] == ["gh", "pr", "ready"]
+    ]
+    if reset_fails:
+        assert result.returncode == 42, result.stdout + result.stderr
+        assert not pushes
+    else:
+        assert result.returncode == 0, result.stdout + result.stderr
+        assert len(pushes) == 1
+        if pr_state == "ready":
+            assert len(resets) == 1 and resets[0] < pushes[0]
+            assert json.loads(state_path.read_text())["status"] == "draft"
+        else:
+            assert not resets
+    assert step["env"]["GH_TOKEN"] == "${{ steps.app-token.outputs.token }}"
+
+
+# --- Attempt lifecycle -----------------------------------------------------------------
+
+
+@pytest.mark.parametrize("abandonment", ["expired", "failed_owner"])
+def test_abandoned_stage_recovers_without_reusing_evidence(tmp_path, abandonment):
+    from policyengine.provenance.qualification_ledger import (
+        attempt_directory,
+        read_json,
+    )
+    from policyengine.provenance.uk_qualification import (
+        attempt_is_recoverable,
+        attempt_key,
+        stage_attempt,
+    )
+
+    pair = make_pair(model="2.200.0")
+    base = "a" * 40
+    attempt = attempt_directory(tmp_path, "uk", pair.pair_id, base[:12])
+    payload = {"pair": pair.canonical_payload(), "base_commit": base}
+    stage_attempt(attempt, payload, HARNESS)
+    # The owner fails before request-replay, leaving durable but unfinished work.
+    write_json(
+        attempt / "staging.json",
+        {
+            "sequence": 1,
+            "owner_run_id": "42",
+            "staged_at": "2026-10-14T10:00:00Z",
+            "expires_at": "2026-10-14T14:00:00Z",
+        },
+    )
+    original = read_json(attempt / "candidate.json")
+    now = "2026-10-14T15:00:00Z" if abandonment == "expired" else "2026-10-14T11:00:00Z"
+
+    def failed(run_id):
+        return abandonment == "failed_owner" and run_id == "42"
+
+    assert attempt_is_recoverable(attempt, now=now, owner_is_failed=failed)
+    assert (
+        candidate_action(
+            pair, BUNDLE, tmp_path, base_commit=base, now=now, owner_is_failed=failed
+        )[0]
+        == "qualify"
+    )
+    new_key = attempt_key(
+        pair, tmp_path, base_commit=base, now=now, owner_is_failed=failed
+    )
+    assert new_key == base[:12] + "-2"
+    new_attempt = attempt_directory(tmp_path, "uk", pair.pair_id, new_key)
+    stage_attempt(new_attempt, payload, HARNESS, owner_run_id="43", staged_at=now)
+    assert read_json(attempt / "candidate.json") == original
+    assert (
+        candidate_action(
+            pair, BUNDLE, tmp_path, base_commit=base, now=now, owner_is_failed=failed
+        )[0]
+        == "duplicate"
+    )
+
+
+@pytest.mark.parametrize("protected", ["live", "request", "terminal", "missing_stage"])
+def test_recovery_preserves_live_sent_and_terminal_attempts(tmp_path, protected):
+    from policyengine.provenance.uk_qualification import (
+        attempt_is_recoverable,
+        stage_attempt,
+    )
+
+    pair = make_pair()
+    attempt = tmp_path / "attempt"
+    stage_attempt(attempt, {"pair": pair.canonical_payload()}, HARNESS)
+    write_json(
+        attempt / "staging.json",
+        {
+            "sequence": 1,
+            "owner_run_id": "42",
+            "expires_at": "2026-10-14T14:00:00Z",
+        },
+    )
+    now = "2026-10-14T15:00:00Z"
+    if protected == "live":
+        now = "2026-10-14T11:00:00Z"
+    elif protected == "request":
+        write_json(attempt / "replay" / "request.json", {"sent": True})
+    elif protected == "terminal":
+        write_json(
+            attempt / "qualification_receipt.json", make_receipt().canonical_payload()
+        )
+    else:
+        (attempt / "staging.json").unlink()
+    assert not attempt_is_recoverable(attempt, now=now, owner_is_failed=lambda _: False)
+
+
+def test_stage_records_owner_and_expiry_immutably(tmp_path):
+    from policyengine.provenance.qualification_ledger import read_json
+    from policyengine.provenance.uk_qualification import stage_attempt
+
+    payload = {"pair": make_pair().canonical_payload()}
+    stage_attempt(
+        tmp_path, payload, HARNESS, owner_run_id="42", staged_at="2026-10-14T10:00:00Z"
+    )
+    stage = read_json(tmp_path / "staging.json")
+    assert stage["owner_run_id"] == "42"
+    assert stage["expires_at"] == "2026-10-14T14:00:00Z"
+    stage_attempt(tmp_path, payload, HARNESS, owner_run_id="43")
+    assert read_json(tmp_path / "staging.json") == stage
+
+
+def test_unowned_legacy_stage_is_recoverable_on_the_same_base(tmp_path):
+    from policyengine.provenance.qualification_ledger import attempt_directory
+    from policyengine.provenance.uk_qualification import stage_attempt
+
+    pair = make_pair(model="2.200.0")
+    base = "a" * 40
+    attempt = attempt_directory(tmp_path, "uk", pair.pair_id, base[:12])
+    stage_attempt(attempt, {"pair": pair.canonical_payload()}, HARNESS)
+    write_json(attempt / "staging.json", {"sequence": 1})
+    assert candidate_action(pair, BUNDLE, tmp_path, base_commit=base)[0] == "qualify"
+
+
+def test_newer_abandoned_stage_recovers_despite_an_older_replay_request(tmp_path):
+    from policyengine.provenance.qualification_ledger import attempt_directory
+    from policyengine.provenance.uk_qualification import attempt_key, stage_attempt
+
+    pair = make_pair(model="2.200.0")
+    base = "a" * 40
+    payload = {"pair": pair.canonical_payload()}
+    earlier = attempt_directory(tmp_path, "uk", pair.pair_id, base[:12])
+    stage_attempt(
+        earlier, payload, HARNESS, owner_run_id="41", staged_at="2026-10-14T09:00:00Z"
+    )
+    write_json(earlier / "replay" / "request.json", {"sent": True})
+    newer = attempt_directory(tmp_path, "uk", pair.pair_id, base[:12] + "-2")
+    stage_attempt(
+        newer, payload, HARNESS, owner_run_id="42", staged_at="2026-10-14T10:00:00Z"
+    )
+    now = "2026-10-14T15:00:00Z"
+    assert (
+        candidate_action(pair, BUNDLE, tmp_path, base_commit=base, now=now)[0]
+        == "qualify"
+    )
+    assert attempt_key(pair, tmp_path, base_commit=base, now=now) == base[:12] + "-3"
+    assert (earlier / "replay" / "request.json").exists()
+
+
+def _publish_as_attempt(ledger, pair, receipt, key):
+    """Publish ``receipt`` as an earlier, unstaged attempt's own receipt.
+
+    A published receipt counts only as its latest attempt's receipt
+    (``published_receipt``), so the fixture records the attempt it came from.
+    """
+
+    pair_dir = ledger / "uk" / pair.pair_id
+    write_json(
+        pair_dir / "attempts" / key / "qualification_receipt.json",
+        receipt.canonical_payload(),
+    )
+    write_json(pair_dir / "qualification_receipt.json", receipt.canonical_payload())
+
+
+@pytest.mark.parametrize("verdict", ["qualified", "rejected"])
+@pytest.mark.parametrize("main_advanced", [False, True])
+@pytest.mark.parametrize("staged_base", ["a" * 40, "f" * 40])
+def test_forced_stage_recovery_binds_the_terminal_receipt_it_supersedes(
+    tmp_path, verdict, main_advanced, staged_base, monkeypatch, capsys
+):
+    import importlib.util
+
+    from policyengine.provenance.qualification_ledger import attempt_directory
+    from policyengine.provenance.uk_qualification import stage_attempt
+
+    pair = make_pair(model="2.200.0")
+    results = gates(wheel_hashes="fail") if verdict == "rejected" else gates()
+    terminal = make_receipt(pair=pair, gate_results=results)
+    base = staged_base
+    _publish_as_attempt(tmp_path, pair, terminal, "terminal")
+    attempt = attempt_directory(tmp_path, "uk", pair.pair_id, base[:12] + "-2")
+    payload = {
+        "pair": pair.canonical_payload(),
+        "supersedes_receipt_sha256": terminal.unreviewed_sha256,
+    }
+    stage_attempt(
+        attempt, payload, HARNESS, owner_run_id="42", staged_at="2026-10-14T10:00:00Z"
+    )
+    if main_advanced:
+        base = "b" * 40
+    now = "2026-10-14T15:00:00Z"
+    assert (
+        candidate_action(pair, BUNDLE, tmp_path, base_commit=base, now=now)[0]
+        == "qualify"
+    )
+    spec = importlib.util.spec_from_file_location(
+        "check_uk_bound_recovery_for_test", REPO_ROOT / "scripts/check_uk_candidate.py"
+    )
+    cli = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(cli)
+    monkeypatch.setattr(cli, "utc_now", lambda: now)
+    assert cli.main(["recoverable", "--ledger", str(tmp_path)]) == 0
+    assert json.loads(capsys.readouterr().out) == payload
+    changed = make_receipt(pair=pair, gate_results=gates(replay="fail"))
+    _publish_as_attempt(tmp_path, pair, changed, "terminal-later")
+    assert (
+        candidate_action(pair, BUNDLE, tmp_path, base_commit=base, now=now)[0]
+        == "duplicate"
+    )
+    with pytest.raises(ValueError, match="exact receipt"):
+        stage_attempt(attempt.parent / (base[:12] + "-3"), payload, HARNESS)
+    assert cli.main(["recoverable", "--ledger", str(tmp_path)]) == 0
+    assert capsys.readouterr().out == ""
+
+
+@pytest.mark.parametrize("protected", ["live", "request", "terminal", "unbound"])
+def test_forced_recovery_preserves_newer_work_on_another_base(tmp_path, protected):
+    from policyengine.provenance.qualification_ledger import attempt_directory
+    from policyengine.provenance.uk_qualification import stage_attempt
+
+    pair = make_pair(model="2.200.0")
+    terminal = make_receipt(pair=pair)
+    write_json(
+        tmp_path / "uk" / pair.pair_id / "qualification_receipt.json",
+        terminal.canonical_payload(),
+    )
+    original_base, replacement_base = "f" * 40, "b" * 40
+    payload = {
+        "pair": pair.canonical_payload(),
+        "supersedes_receipt_sha256": terminal.unreviewed_sha256,
+    }
+    original = attempt_directory(tmp_path, "uk", pair.pair_id, original_base[:12])
+    stage_attempt(
+        original, payload, HARNESS, owner_run_id="42", staged_at="2026-10-14T10:00:00Z"
+    )
+    replacement = attempt_directory(tmp_path, "uk", pair.pair_id, replacement_base[:12])
+    replacement_payload = (
+        {"pair": pair.canonical_payload()} if protected == "unbound" else payload
+    )
+    stage_attempt(
+        replacement,
+        replacement_payload,
+        HARNESS,
+        owner_run_id="43",
+        staged_at=(
+            "2026-10-14T14:00:00Z" if protected == "live" else "2026-10-14T10:00:00Z"
+        ),
+    )
+    if protected == "request":
+        write_json(replacement / "replay" / "request.json", {"sent": True})
+    elif protected == "terminal":
+        write_json(
+            replacement / "qualification_receipt.json", terminal.canonical_payload()
+        )
+    for base in (original_base, replacement_base, "c" * 40):
+        assert (
+            candidate_action(
+                pair, BUNDLE, tmp_path, base_commit=base, now="2026-10-14T15:00:00Z"
+            )[0]
+            == "duplicate"
+        )
+
+
+@pytest.mark.parametrize("verdict", ["qualified", "rejected"])
+@pytest.mark.parametrize("main_advanced", [False, True])
+@pytest.mark.parametrize("abandonment", ["expired", "failed_owner"])
+def test_resolution_preserves_authorization_to_recover_forced_work(
+    tmp_path, monkeypatch, verdict, main_advanced, abandonment
+):
+    import importlib.util
+
+    from policyengine.provenance.qualification_ledger import attempt_directory
+    from policyengine.provenance.uk_qualification import stage_attempt
+
+    spec = importlib.util.spec_from_file_location(
+        "check_uk_resolve_recovery_for_test",
+        REPO_ROOT / "scripts/check_uk_candidate.py",
+    )
+    cli = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(cli)
+    pair = make_pair(model="2.200.0")
+    terminal = make_receipt(
+        pair=pair,
+        gate_results=gates(replay="fail") if verdict == "rejected" else gates(),
+    )
+    _publish_as_attempt(tmp_path, pair, terminal, "terminal")
+    base = "a" * 40
+    resolved = cli.ResolvedCandidate(
+        pair,
+        cli.WheelFile(
+            "model.whl", "https://example.org/model.whl", pair.model.wheel_sha256
+        ),
+        cli.WheelFile(
+            "core.whl", "https://example.org/core.whl", pair.core.wheel_sha256
+        ),
+        "hf://model/example@pin/manifest.json",
+        ">=3.33.0",
+    )
+    monkeypatch.setattr(cli, "resolve_candidate", lambda **kwargs: resolved)
+    monkeypatch.setattr(cli, "git", lambda *args: base)
+    monkeypatch.setattr(
+        cli,
+        "owner_run_failed",
+        lambda run_id: abandonment == "failed_owner" and run_id == "42",
+    )
+    now = "2026-10-14T15:00:00Z" if abandonment == "expired" else "2026-10-14T11:00:00Z"
+    monkeypatch.setattr(cli, "utc_now", lambda: now)
+    monkeypatch.setattr(cli, "set_output", lambda **kwargs: None)
+    out = tmp_path / "candidate.json"
+    args = SimpleNamespace(
+        model_version=None,
+        data_version=None,
+        ledger=str(tmp_path),
+        force=True,
+        out=str(out),
+    )
+    assert cli.cmd_resolve(args) == 0
+    payload = json.loads(out.read_text())
+    assert payload["supersedes_receipt_sha256"] == terminal.unreviewed_sha256
+    attempt = attempt_directory(tmp_path, "uk", pair.pair_id, payload["attempt_key"])
+    stage_attempt(
+        attempt, payload, HARNESS, owner_run_id="42", staged_at="2026-10-14T10:00:00Z"
+    )
+    original_candidate = (attempt / "candidate.json").read_bytes()
+    if main_advanced:
+        base = "b" * 40
+    args.force = False
+    assert cli.cmd_resolve(args) == 0
+    recovery = json.loads(out.read_text())
+    assert recovery["action"] == "qualify"
+    assert recovery["base_commit"] == base
+    assert recovery["attempt_key"] == (base[:12] if main_advanced else base[:12] + "-2")
+    assert recovery["supersedes_receipt_sha256"] == terminal.unreviewed_sha256
+    replacement = attempt_directory(
+        tmp_path, "uk", pair.pair_id, recovery["attempt_key"]
+    )
+    stage_attempt(replacement, recovery, HARNESS, owner_run_id="43", staged_at=now)
+    assert (attempt / "candidate.json").read_bytes() == original_candidate
+    assert cli.cmd_resolve(args) == 0
+    assert json.loads(out.read_text())["action"] == "duplicate"
+
+
+def test_cross_base_recovery_allocates_new_keys_without_overwriting_evidence(tmp_path):
+    from policyengine.provenance.qualification_ledger import attempt_directory
+    from policyengine.provenance.uk_qualification import attempt_key, stage_attempt
+
+    pair = make_pair(model="2.200.0")
+    old_base, current_base = "a" * 40, "b" * 40
+    existing = attempt_directory(tmp_path, "uk", pair.pair_id, current_base[:12])
+    sent_payload = {"pair": pair.canonical_payload(), "base_commit": current_base}
+    stage_attempt(
+        existing,
+        sent_payload,
+        HARNESS,
+        owner_run_id="41",
+        staged_at="2026-10-14T09:00:00Z",
+    )
+    write_json(existing / "replay" / "request.json", {"sent": True})
+    abandoned = attempt_directory(tmp_path, "uk", pair.pair_id, old_base[:12])
+    stage_attempt(
+        abandoned,
+        {"pair": pair.canonical_payload(), "base_commit": old_base},
+        HARNESS,
+        owner_run_id="42",
+        staged_at="2026-10-14T10:00:00Z",
+    )
+    now = "2026-10-14T15:00:00Z"
+    assert (
+        candidate_action(pair, BUNDLE, tmp_path, base_commit=current_base, now=now)[0]
+        == "qualify"
+    )
+    assert attempt_key(pair, tmp_path, base_commit=current_base, now=now) == (
+        current_base[:12] + "-2"
+    )
+    assert json.loads((existing / "candidate.json").read_text()) == sent_payload
+    assert json.loads((existing / "replay" / "request.json").read_text()) == {
+        "sent": True
+    }
+
+
+@pytest.mark.parametrize(
+    ("status", "conclusion", "expected"),
+    [
+        ("completed", "failure", True),
+        ("completed", "cancelled", True),
+        ("completed", "timed_out", True),
+        ("in_progress", "failure", False),
+        ("completed", "success", False),
+        ("queued", None, False),
+    ],
+)
+def test_recovery_requires_a_confirmed_failed_owner(
+    monkeypatch, status, conclusion, expected
+):
+    import importlib.util
+
+    spec = importlib.util.spec_from_file_location(
+        "check_uk_owner_for_test", REPO_ROOT / "scripts/check_uk_candidate.py"
+    )
+    cli = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(cli)
+    calls = []
+
+    def run(argv, **kwargs):
+        calls.append(argv)
+        return subprocess.CompletedProcess(
+            argv, 0, json.dumps({"status": status, "conclusion": conclusion})
+        )
+
+    monkeypatch.setattr(cli, "subprocess", SimpleNamespace(run=run))
+    assert cli.owner_run_failed("42") is expected
+    assert "42" in calls[0] and cli.LEDGER_REPOSITORY in calls[0]
+    cli.owner_run_failed.cache_clear()
+    monkeypatch.setattr(
+        cli,
+        "subprocess",
+        SimpleNamespace(run=lambda *a, **k: subprocess.CompletedProcess(a, 1, "")),
+    )
+    assert not cli.owner_run_failed("42")
+
+
+def test_catchup_finds_abandoned_stages_without_replay_requests(
+    tmp_path, monkeypatch, capsys
+):
+    import importlib.util
+
+    from policyengine.provenance.qualification_ledger import attempt_directory
+    from policyengine.provenance.uk_qualification import stage_attempt
+
+    spec = importlib.util.spec_from_file_location(
+        "check_uk_recovery_for_test", REPO_ROOT / "scripts/check_uk_candidate.py"
+    )
+    cli = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(cli)
+    pair = make_pair()
+    attempt = attempt_directory(tmp_path, "uk", pair.pair_id, "a" * 12)
+    payload = {"pair": pair.canonical_payload()}
+    stage_attempt(attempt, payload, HARNESS)
+    write_json(
+        attempt / "staging.json",
+        {
+            "sequence": 1,
+            "owner_run_id": "42",
+            "expires_at": "2026-10-14T14:00:00Z",
+        },
+    )
+    monkeypatch.setattr(cli, "utc_now", lambda: "2026-10-14T11:00:00Z")
+    monkeypatch.setattr(cli, "owner_run_failed", lambda run_id: run_id == "42")
+    assert cli.main(["recoverable", "--ledger", str(tmp_path)]) == 0
+    assert json.loads(capsys.readouterr().out) == payload
+    replacement = attempt_directory(tmp_path, "uk", pair.pair_id, "a" * 12 + "-2")
+    stage_attempt(
+        replacement,
+        payload,
+        HARNESS,
+        owner_run_id="43",
+        staged_at="2026-10-14T11:00:00Z",
+    )
+    assert cli.main(["recoverable", "--ledger", str(tmp_path)]) == 0
+    assert capsys.readouterr().out == ""
+    write_json(
+        attempt / "staging.json",
+        {
+            "sequence": 3,
+            "owner_run_id": "42",
+            "expires_at": "2026-10-14T14:00:00Z",
+        },
+    )
+    write_json(
+        tmp_path / "uk" / pair.pair_id / "qualification_receipt.json",
+        make_receipt(pair=pair).canonical_payload(),
+    )
+    assert cli.main(["recoverable", "--ledger", str(tmp_path)]) == 0
+    assert capsys.readouterr().out == ""
+
+
+def test_attempt_lifecycle(tmp_path):
+    from policyengine.provenance.qualification import GateResult
+    from policyengine.provenance.qualification_ledger import (
+        attempt_directory,
+        read_json,
+    )
+    from policyengine.provenance.uk_qualification import (
+        attempt_is_waiting_for_replay,
+        finalize_attempt,
+        stage_attempt,
+        write_gate_results,
+    )
+
+    pair = make_pair()
+    attempt = attempt_directory(tmp_path, "uk", pair.pair_id, "abcdef123456")
+    stage_attempt(
+        attempt,
+        {
+            "pair": pair.canonical_payload(),
+            "pair_id": pair.pair_id,
+            "previous_certificate": None,
+        },
+        HARNESS,
+    )
+    local = [gate for gate in gates() if gate.gate_id != "replay"]
+    write_gate_results(attempt, local, {gate.gate_id: {"ok": True} for gate in local})
+    assert not attempt_is_waiting_for_replay(attempt)
+    receipt = finalize_attempt(
+        attempt, tmp_path, issued_by=ISSUED, completed_at="2026-10-15T10:00:00Z"
+    )
+    assert receipt.verdict == "incomplete"  # the replay never ran
+    assert (
+        read_json(tmp_path / "uk" / pair.pair_id / "qualification_receipt.json")[
+            "verdict"
+        ]
+        == "incomplete"
+    )
+
+    request = build_replay_request(
+        pair,
+        HARNESS,
+        release_bundle=golden("qualification_request_golden")["candidate"][
+            "expected_release_bundle"
+        ],
+        issued_by=ISSUED,
+    )
+    write_json(attempt / "replay" / "request.json", request.document())
+    assert attempt_is_waiting_for_replay(attempt)
+    write_json(
+        attempt / "replay" / "replay.result.json",
+        GateResult(gate_id="replay", status="pass", summary="ok").canonical_payload(),
+    )
+    assert not attempt_is_waiting_for_replay(attempt)
+    final = finalize_attempt(
+        attempt, tmp_path, issued_by=ISSUED, completed_at="2026-10-15T11:00:00Z"
+    )
+    assert final.verdict == "qualified"
+    assert final.replay.request_id == request.request_id
+    assert (attempt / "receipts" / f"{receipt.receipt_sha256}.json").is_file()
+    assert (attempt / "receipts" / f"{final.receipt_sha256}.json").is_file()
+    assert (
+        read_json(attempt / "receipts" / f"{receipt.receipt_sha256}.json")
+        == receipt.canonical_payload()
+    )
+
+
+def test_older_attempt_finalization_preserves_latest_staged_attempt(tmp_path):
+    from policyengine.provenance.qualification_ledger import (
+        attempt_directory,
+        read_json,
+    )
+    from policyengine.provenance.uk_qualification import (
+        finalize_attempt,
+        stage_attempt,
+        write_gate_results,
+    )
+
+    pair = make_pair()
+    # The staging order deliberately differs from directory/commit order.
+    older = attempt_directory(tmp_path, "uk", pair.pair_id, "ffffffffffff")
+    newer = attempt_directory(tmp_path, "uk", pair.pair_id, "aaaaaaaaaaaa")
+    payload = {"pair": pair.canonical_payload(), "previous_certificate": None}
+    stage_attempt(older, payload, HARNESS)
+    newer_harness = HARNESS.model_copy(update={"git_commit": "a" * 40})
+    stage_attempt(newer, payload, newer_harness)
+    for attempt in (older, newer):
+        write_gate_results(attempt, gates(), {})
+    newest = finalize_attempt(
+        newer, tmp_path, issued_by=ISSUED, completed_at="2026-10-15T10:00:00Z"
+    )
+    old = finalize_attempt(
+        older, tmp_path, issued_by=ISSUED, completed_at="2026-10-15T11:00:00Z"
+    )
+    latest = read_json(tmp_path / "uk" / pair.pair_id / "qualification_receipt.json")
+    assert latest == newest.canonical_payload()
+    assert read_json(older / "qualification_receipt.json") == old.canonical_payload()
+    assert read_json(newer / "qualification_receipt.json") == newest.canonical_payload()
+
+
+def test_terminal_attempt_receipt_is_immutable_and_finalization_is_idempotent(tmp_path):
+    from policyengine.provenance.qualification_ledger import attempt_directory
+    from policyengine.provenance.uk_qualification import (
+        finalize_attempt,
+        stage_attempt,
+        write_gate_results,
+    )
+
+    pair = make_pair()
+    attempt = attempt_directory(tmp_path, "uk", pair.pair_id, "abcdef123456")
+    stage_attempt(attempt, {"pair": pair.canonical_payload()}, HARNESS)
+    write_gate_results(attempt, gates(), {})
+    original = finalize_attempt(
+        attempt, tmp_path, issued_by=ISSUED, completed_at="2026-10-15T10:00:00Z"
+    )
+    original_bytes = (attempt / "qualification_receipt.json").read_bytes()
+    write_gate_results(attempt, gates(replay="fail"), {})
+    repeated = finalize_attempt(
+        attempt, tmp_path, issued_by=ISSUED, completed_at="2026-10-15T11:00:00Z"
+    )
+    assert repeated == original
+    assert (attempt / "qualification_receipt.json").read_bytes() == original_bytes
+
+
+def test_new_staged_attempt_advances_a_legacy_latest_receipt(tmp_path):
+    from policyengine.provenance.qualification_ledger import (
+        attempt_directory,
+        read_json,
+    )
+    from policyengine.provenance.uk_qualification import (
+        finalize_attempt,
+        stage_attempt,
+        write_gate_results,
+    )
+
+    legacy = make_receipt()
+    latest_path = tmp_path / "uk" / legacy.pair_id / "qualification_receipt.json"
+    write_json(latest_path, legacy.canonical_payload())
+    attempt = attempt_directory(tmp_path, "uk", legacy.pair_id, "new-attempt")
+    stage_attempt(attempt, {"pair": legacy.pair.canonical_payload()}, HARNESS)
+    write_gate_results(attempt, gates(), {})
+    current = finalize_attempt(
+        attempt, tmp_path, issued_by=ISSUED, completed_at="2026-10-15T11:00:00Z"
+    )
+    assert read_json(latest_path) == current.canonical_payload()
+
+
+@pytest.mark.parametrize(
+    "interrupted", ["missing", "older", "missing-pointer", "missing-receipt"]
+)
+def test_terminal_finalization_repairs_interrupted_latest_publication(
+    tmp_path, interrupted
+):
+    from policyengine.provenance.qualification_ledger import (
+        attempt_directory,
+        read_json,
+    )
+    from policyengine.provenance.uk_qualification import (
+        finalize_attempt,
+        stage_attempt,
+        write_gate_results,
+    )
+
+    pair = make_pair()
+    payload = {"pair": pair.canonical_payload()}
+    older = attempt_directory(tmp_path, "uk", pair.pair_id, "older")
+    stage_attempt(older, payload, HARNESS)
+    write_gate_results(older, gates(), {})
+    old_receipt = finalize_attempt(
+        older, tmp_path, issued_by=ISSUED, completed_at="2026-10-15T09:00:00Z"
+    )
+    current = attempt_directory(tmp_path, "uk", pair.pair_id, "newer")
+    stage_attempt(current, payload, HARNESS.model_copy(update={"git_commit": "a" * 40}))
+    write_gate_results(current, gates(), {})
+    receipt = finalize_attempt(
+        current, tmp_path, issued_by=ISSUED, completed_at="2026-10-15T10:00:00Z"
+    )
+    original_bytes = (current / "qualification_receipt.json").read_bytes()
+    pair_dir = tmp_path / "uk" / pair.pair_id
+    latest = pair_dir / "qualification_receipt.json"
+    pointer = pair_dir / "latest_attempt.json"
+    if interrupted == "older":
+        write_json(latest, old_receipt.canonical_payload())
+        write_json(pointer, {"attempt_key": "older", "sequence": 1})
+    else:
+        if interrupted != "missing-pointer":
+            latest.unlink()
+        if interrupted != "missing-receipt":
+            pointer.unlink()
+    repeated = finalize_attempt(
+        current, tmp_path, issued_by=ISSUED, completed_at="2026-10-15T11:00:00Z"
+    )
+    assert repeated == receipt
+    assert (current / "qualification_receipt.json").read_bytes() == original_bytes
+    assert read_json(latest) == receipt.canonical_payload()
+    assert read_json(pointer) == {"attempt_key": "newer", "sequence": 2}
+    finalize_attempt(
+        older, tmp_path, issued_by=ISSUED, completed_at="2026-10-15T12:00:00Z"
+    )
+    assert read_json(latest) == receipt.canonical_payload()
+
+
+def test_stale_latest_pointer_cannot_replace_a_newer_incomplete_receipt(tmp_path):
+    from policyengine.provenance.qualification_ledger import (
+        attempt_directory,
+        read_json,
+    )
+    from policyengine.provenance.uk_qualification import (
+        finalize_attempt,
+        stage_attempt,
+        write_gate_results,
+    )
+
+    pair = make_pair()
+    payload = {"pair": pair.canonical_payload()}
+    older = attempt_directory(tmp_path, "uk", pair.pair_id, "older")
+    stage_attempt(older, payload, HARNESS)
+    write_gate_results(older, gates(), {})
+    finalize_attempt(
+        older, tmp_path, issued_by=ISSUED, completed_at="2026-10-15T09:00:00Z"
+    )
+    newer = attempt_directory(tmp_path, "uk", pair.pair_id, "newer")
+    stage_attempt(newer, payload, HARNESS.model_copy(update={"git_commit": "a" * 40}))
+    write_gate_results(newer, gates(replay="unavailable"), {})
+    current = finalize_attempt(
+        newer, tmp_path, issued_by=ISSUED, completed_at="2026-10-15T10:00:00Z"
+    )
+    pair_dir = tmp_path / "uk" / pair.pair_id
+    write_json(
+        pair_dir / "latest_attempt.json", {"attempt_key": "older", "sequence": 1}
+    )
+    finalize_attempt(
+        older, tmp_path, issued_by=ISSUED, completed_at="2026-10-15T11:00:00Z"
+    )
+    assert (
+        read_json(pair_dir / "qualification_receipt.json")
+        == current.canonical_payload()
+    )
+    assert read_json(pair_dir / "latest_attempt.json") == {
+        "attempt_key": "newer",
+        "sequence": 2,
+    }
+
+
+# --- Release dispatch ----------------------------------------------------------------
+
+
+def test_release_payload_keeps_version_first_and_carries_identity():
+    from policyengine.provenance.receipt import certificate_sha256
+    from policyengine.provenance.uk_qualification import release_dispatch_payload
+
+    version = BUNDLE["policyengine_version"]
+    payload = release_dispatch_payload(BUNDLE, version=version)
+    assert payload["event_type"] == "policyengine-release"
+    body = payload["client_payload"]
+    assert list(body)[0] == "version" and body["version"] == version
+    uk = body["bundle"]["uk"]
+    record = BUNDLE["data_releases"]["uk"]
+    assert uk["model_version"] == record["model_package"]["version"]
+    assert uk["dataset_sha256"] == record["certified_data_artifact"]["sha256"]
+    assert uk["certificate_sha256"] == certificate_sha256(record)
+    assert body["qualification"] is None  # the committed pair was not auto-qualified
+    assert len(body) <= 10  # repository_dispatch limit on top-level keys
+    with pytest.raises(ValueError):
+        release_dispatch_payload(BUNDLE, version="0.0.0")
+
+
+def test_release_payload_names_the_qualification_receipt():
+    from policyengine.provenance.uk_qualification import release_dispatch_payload
+
+    bundle = json.loads(json.dumps(BUNDLE))
+    receipt = make_receipt()
+    bundle["data_releases"]["uk"]["certification"].update(
+        compatibility_basis="qualified_exact_pair",
+        qualification_pair_id=receipt.pair_id,
+    )
+    payload = release_dispatch_payload(
+        bundle,
+        version=bundle["policyengine_version"],
+        fetch_receipt=lambda country, pair_id: receipt,
+    )
+    assert payload["client_payload"]["qualification"] == {
+        "uk": {
+            "pair_id": receipt.pair_id,
+            "verdict": "qualified",
+            "promotable": True,
+            "receipt_sha256": receipt.receipt_sha256,
+        }
+    }
+
+
+def test_released_bundle_request_is_valid_for_the_scorecard():
+    from policyengine.provenance.uk_qualification import released_bundle_request
+
+    record = BUNDLE["data_releases"]["uk"]
+    dataset = record["default_dataset"]
+    manifest = {
+        "schema_version": 1,
+        "data_package": {
+            "name": "policyengine-uk-data",
+            "version": record["data_package"]["version"],
+        },
+        "default_datasets": {"national": dataset},
+        "artifacts": {
+            dataset: {
+                "kind": "microdata",
+                "path": record["datasets"][dataset]["path"],
+                "repo_id": record["datasets"][dataset]["repo_id"],
+                "revision": record["datasets"][dataset]["revision"],
+                "sha256": record["datasets"][dataset]["sha256"],
+                "size_bytes": 128369110,
+            }
+        },
+    }
+    core = BUNDLE["packages"]["policyengine-core"]["version"]
+    request = released_bundle_request(
+        BUNDLE,
+        version=BUNDLE["policyengine_version"],
+        wheel_sha256=sha("7"),
+        packaged_manifest_sha256=sha("8"),
+        release_bundle=golden("qualification_request_golden")["candidate"][
+            "expected_release_bundle"
+        ],
+        hf_file=lambda repo, rev, path: json.dumps(manifest).encode(),
+        pypi_project=lambda name: {
+            "releases": {core: [_wheel("policyengine-core", core, sha("6"))]}
+        },
+        requested_by={
+            "repository": "PolicyEngine/policyengine.py",
+            "workflow_run_id": 5,
+            "commit": "e" * 40,
+        },
+    )
+    schema = json.loads(
+        (VENDORED_SCHEMAS / "qualification_request.v1.json").read_text()
+    )
+    jsonschema.Draft202012Validator(schema).validate(request.document())
+    assert request.kind == "released_bundle"
+    assert request.document()["candidate"]["policyengine"]["source"] == {
+        "pypi": {"wheel_sha256": sha("7")}
+    }
+
+
+def test_expected_release_bundle_uses_the_scorecard_certification_environment(
+    monkeypatch, tmp_path
+):
+    """W6 interface point 4: no tokens, no proxy exceptions, no network."""
+
+    from policyengine.provenance import uk_qualification
+
+    environ = {
+        "PATH": "/usr/bin",
+        "HF_TOKEN": "t",
+        "HUGGING_FACE_TOKEN": "t",
+        "HUGGINGFACE_HUB_TOKEN": "t",
+        "NO_PROXY": "huggingface.co",
+        "no_proxy": "huggingface.co",
+        "HTTPS_PROXY": "http://corporate:3128",
+        "POLICYENGINE_SKIP_COUNTRY_IMPORTS": "1",
+    }
+    env = uk_qualification.certification_environment(environ)
+    assert env["PATH"] == "/usr/bin"
+    for name in ("HF_TOKEN", "HUGGING_FACE_TOKEN", "HUGGINGFACE_HUB_TOKEN"):
+        assert name not in env
+    assert "NO_PROXY" not in env and "no_proxy" not in env
+    for name in ("HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY"):
+        assert env[name] == env[name.lower()] == "http://127.0.0.1:9"
+    assert env["HF_HUB_OFFLINE"] == "1"
+
+    seen = {}
+
+    def run(args, *, cwd, env, check, capture_output, text):
+        seen["env"] = env
+        return type("Done", (), {"stdout": '{"policyengine_version": "6.2.6"}\n'})()
+
+    monkeypatch.setattr(uk_qualification.subprocess, "run", run)
+    monkeypatch.setattr(uk_qualification.os, "environ", environ)
+    assert uk_qualification.expected_release_bundle("python", cwd=tmp_path) == {
+        "policyengine_version": "6.2.6"
+    }
+    assert seen["env"] == env
+
+
+def test_the_certification_environment_falls_back_to_the_packaged_certificate():
+    """Executed: with the proxies, the manifest request fails and the bundled
+    certification is used (the mechanism W6's recipe relies on)."""
+
+    import os
+    import subprocess
+    import sys
+
+    from policyengine.provenance import uk_qualification
+
+    script = (
+        "import json\n"
+        "from policyengine.provenance import manifest\n"
+        "try:\n"
+        "    manifest.get_data_release_manifest('uk')\n"
+        "    print('reached')\n"
+        "except manifest.DataReleaseManifestUnavailableError:\n"
+        "    print('unavailable')\n"
+    )
+    result = subprocess.run(
+        [sys.executable, "-c", script],
+        env=uk_qualification.certification_environment(os.environ),
+        capture_output=True,
+        text=True,
+        timeout=60,
+    )
+    assert result.stdout.strip().splitlines()[-1] == "unavailable", result.stderr
diff --git a/tests/test_uk_qualification_workflow.py b/tests/test_uk_qualification_workflow.py
new file mode 100644
index 00000000..88b57b4c
--- /dev/null
+++ b/tests/test_uk_qualification_workflow.py
@@ -0,0 +1,403 @@
+"""Qualification workflow readiness and failed-check refresh (no remote writes)."""
+
+from __future__ import annotations
+
+import argparse
+import importlib.util
+import json
+import os
+import subprocess
+import sys
+from pathlib import Path
+from types import SimpleNamespace
+
+import pytest
+import yaml
+
+from policyengine.provenance.uk_qualification import (
+    accept_review,
+    candidate_branch,
+    previous_certificate,
+)
+from tests.qualification_fixtures import gates, make_pair, make_receipt
+
+REPO_ROOT = Path(__file__).resolve().parents[1]
+WORKFLOW_PATH = REPO_ROOT / ".github/workflows/certify-uk-release.yaml"
+
+
+def _cli():
+    spec = importlib.util.spec_from_file_location(
+        "qualification_workflow_cli", REPO_ROOT / "scripts/check_uk_candidate.py"
+    )
+    module = importlib.util.module_from_spec(spec)
+    spec.loader.exec_module(module)
+    return module
+
+
+def _workflow():
+    return yaml.safe_load(WORKFLOW_PATH.read_text())
+
+
+def test_resolution_can_authenticate_the_staged_attempt_owner_lookup():
+    resolve = _workflow()["jobs"]["resolve"]
+    assert resolve["permissions"]["contents"] == "read"
+    assert resolve["permissions"]["actions"] == "read"
+    step = next(
+        step
+        for step in resolve["steps"]
+        if step.get("name") == "Resolve and deduplicate the candidate"
+    )
+    assert step["env"]["GH_TOKEN"] == "${{ github.token }}"
+
+
+def test_every_uv_step_runs_the_environment_its_job_installed():
+    """The repository's .python-version asks for a newer Python than the 3.13
+    environment each job installs. A syncing `uv run` would replace that
+    environment, extras and all, so every step and the ledger re-derivation
+    must run the interpreter the job provisioned."""
+    workflow = _workflow()
+    assert (REPO_ROOT / ".python-version").read_text().strip() != (
+        workflow["env"]["PYTHON_VERSION"]
+    )
+    assert workflow["env"]["UV_PYTHON"] == workflow["env"]["PYTHON_VERSION"]
+    command = "${{ github.workspace }}/scripts/check_uk_candidate.py rederive-ledger"
+    publishing_jobs = []
+    for name, job in workflow["jobs"].items():
+        runs = [step.get("run", "") for step in job["steps"]]
+        synced = any(
+            "uv sync --frozen --extra uk --extra dev --python "
+            "${{ env.PYTHON_VERSION }}" in run
+            for run in runs
+        )
+        system = any("uv pip install --system" in run for run in runs)
+        assert not (synced and system), name
+        for line in (line for run in runs for line in run.splitlines()):
+            if "uv run" in line:
+                assert synced, (name, line)
+                assert "uv run --no-sync " in line, (name, line)
+        if any(".github/qualification-ledger.sh publish" in run for run in runs):
+            publishing_jobs.append(name)
+            rederive = job.get("env", {}).get(
+                "LEDGER_REDERIVE", workflow["env"]["LEDGER_REDERIVE"]
+            )
+            if synced:
+                interpreter = "${{ github.workspace }}/.venv/bin/python"
+            else:
+                assert system, name
+                interpreter = "python"
+            assert rederive == f"{interpreter} {command}", name
+    assert sorted(publishing_jobs) == [
+        "accept",
+        "catch-up",
+        "prepare",
+        "qualify",
+        "replay",
+    ]
+
+
+def test_preparation_syncs_the_candidate_lock_before_using_it():
+    """Snapshots, the bundle check and staging must run in the candidate's
+    environment: synced from the lock preparation regenerates, after it is
+    regenerated, with the 3.13 interpreter and both extras."""
+
+    steps = _workflow()["jobs"]["prepare"]["steps"]
+    runs = [step.get("run", "") for step in steps]
+    sync = "uv sync --frozen --extra uk --extra dev --python ${{ env.PYTHON_VERSION }}"
+
+    def position(text):
+        return next(index for index, run in enumerate(runs) if text in run)
+
+    syncs = [index for index, run in enumerate(runs) if sync in run]
+    regenerate = position("check_uk_candidate.py prepare")
+    assert len(syncs) == 2 and syncs[0] < regenerate < syncs[1]
+    for later in (
+        "tests/test_household_calculator_snapshot.py",
+        "scripts/bundle.py check",
+        "check_uk_candidate.py stage",
+    ):
+        assert syncs[1] < position(later), later
+
+
+@pytest.mark.parametrize("prepare_first", [False, True])
+def test_head_replacement_and_readiness_share_one_serialization(
+    monkeypatch, tmp_path, prepare_first
+):
+    """Replay a branch replacement after the real promotion's head snapshot.
+
+    The scheduler's locks come from the workflow, and preparation executes its
+    actual draft-and-push shell with command stand-ins. Different lock groups
+    allow the reviewer's failing interleaving; one shared group defers it.
+    """
+
+    cli = _cli()
+    workflow = _workflow()
+    jobs = workflow["jobs"]
+    bundle = json.loads(cli.BUNDLE_PATH.read_text())
+    qualified = make_receipt(
+        pair=make_pair(
+            model="2.200.0",
+            data=bundle["data_releases"]["uk"]["data_package"]["version"],
+            core=bundle["packages"]["policyengine-core"]["version"],
+        )
+    ).model_copy(update={"previous_certificate": previous_certificate(bundle)})
+    branch = candidate_branch(qualified.pair)
+    state_path = tmp_path / "state.json"
+    state_path.write_text(
+        json.dumps({"status": "ready", "head": qualified.harness.git_commit})
+    )
+    new_head = "f" * 40
+    binary_dir = tmp_path / "bin"
+    binary_dir.mkdir()
+    fake = f"""#!{sys.executable}
+import json, os, sys
+from pathlib import Path
+path = Path(os.environ["PR_STATE"])
+state = json.loads(path.read_text())
+command, args = Path(sys.argv[0]).name, sys.argv[1:]
+if command == "gh" and args[:2] == ["pr", "list"]:
+    if state["status"] == "ready":
+        print("574")
+elif command == "gh" and args[:2] == ["pr", "ready"]:
+    assert args[2:] == ["574", "--undo"]
+    state["status"] = "draft"
+    path.write_text(json.dumps(state))
+elif command == "git" and args[0] == "push":
+    assert state["status"] == "draft"
+    state["head"] = {new_head!r}
+    path.write_text(json.dumps(state))
+"""
+    for command in ("gh", "git"):
+        executable = binary_dir / command
+        executable.write_text(fake)
+        executable.chmod(0o755)
+    push_step = next(
+        step
+        for step in jobs["prepare"]["steps"]
+        if step.get("name") == "Commit and push the candidate commit"
+    )
+    groups = {
+        job: jobs[job]["concurrency"]["group"].replace(
+            "${{ needs.resolve.outputs.pair_id }}", qualified.pair_id
+        )
+        for job in ("prepare", "promote")
+    }
+    running = set()
+    deferred = []
+    ready_heads = []
+
+    def prepare():
+        subprocess.run(
+            ["bash", "-e", "-o", "pipefail", "-c", push_step["run"]],
+            cwd=tmp_path,
+            env={
+                **os.environ,
+                "PATH": f"{binary_dir}{os.pathsep}{os.environ['PATH']}",
+                "BRANCH": branch,
+                "PAIR_ID": qualified.pair_id,
+                "PR_STATE": str(state_path),
+            },
+            check=True,
+            capture_output=True,
+            timeout=30,
+        )
+
+    def schedule(job, operation):
+        if groups[job] in running:
+            deferred.append((job, operation))
+            return
+        running.add(groups[job])
+        try:
+            operation()
+        finally:
+            running.remove(groups[job])
+
+    def fake_run(argv, **kwargs):
+        if argv[:3] == ["gh", "pr", "list"]:
+            state = json.loads(state_path.read_text())
+            return SimpleNamespace(
+                stdout=json.dumps(
+                    [{"headRefName": branch, "headRefOid": state["head"]}]
+                )
+            )
+        assert argv[:3] == ["gh", "pr", "ready"]
+        if not prepare_first:
+            schedule("prepare", prepare)
+        state = json.loads(state_path.read_text())
+        ready_heads.append(state["head"])
+        state["status"] = "ready"
+        state_path.write_text(json.dumps(state))
+        return SimpleNamespace(returncode=0)
+
+    monkeypatch.setattr(cli, "latest_receipts", lambda root, **_: [qualified])
+    monkeypatch.setattr(cli, "subprocess", SimpleNamespace(run=fake_run))
+    monkeypatch.setattr(cli, "config", lambda: {"promotion": {"automatic": False}})
+    if prepare_first:
+        schedule("prepare", prepare)
+    schedule(
+        "promote",
+        lambda: cli.cmd_promote(argparse.Namespace(ledger=str(tmp_path), apply=True)),
+    )
+    for job, operation in deferred:
+        schedule(job, operation)
+    assert all(head == qualified.harness.git_commit for head in ready_heads)
+    assert json.loads(state_path.read_text()) == {"status": "draft", "head": new_head}
+    assert jobs["prepare"]["concurrency"]["cancel-in-progress"] is False
+    assert jobs["promote"]["concurrency"]["cancel-in-progress"] is False
+
+
+@pytest.mark.parametrize("job", ["replay", "catch-up", "accept"])
+def test_workflow_refreshes_failed_checks_after_publishing_receipts(job):
+    workflow = _workflow()
+    configured = workflow["jobs"][job]
+    refresh = next(
+        step
+        for step in configured["steps"]
+        if "refresh-pr-checks" in step.get("run", "")
+    )
+    assert configured["permissions"]["actions"] == "write"
+    assert refresh["env"]["GH_TOKEN"] == "${{ github.token }}"
+    steps = configured["steps"]
+    publish_positions = [
+        index
+        for index, step in enumerate(steps)
+        if ".github/qualification-ledger.sh publish" in step.get("run", "")
+    ]
+    assert publish_positions
+    assert max(publish_positions) < steps.index(refresh)
+
+
+def test_catch_up_dispatches_recovery_for_the_original_pair(tmp_path):
+    workflow = _workflow()
+    job = workflow["jobs"]["catch-up"]
+    step = next(
+        step for step in job["steps"] if "recoverable --ledger" in step.get("run", "")
+    )
+    pair = make_pair(model="2.200.0", data="1.58.9")
+    candidate_path = tmp_path / "candidate.json"
+    candidate_path.write_text(json.dumps({"pair": pair.canonical_payload()}))
+    command_log = tmp_path / "commands.jsonl"
+    binary_dir = tmp_path / "bin"
+    binary_dir.mkdir()
+    fake = f"""#!{sys.executable}
+import json, os, sys
+from pathlib import Path
+command, args = Path(sys.argv[0]).name, sys.argv[1:]
+if command == "python":
+    if args == ["scripts/check_uk_candidate.py", "recoverable", "--ledger", "ledger"]:
+        assert Path("ledger").is_dir()
+        print(Path(os.environ["RECOVERY_CANDIDATE"]).read_text())
+    else:
+        assert args == ["-"]
+        os.execv(sys.executable, [sys.executable, *args])
+else:
+    assert command == "gh"
+    assert Path("ledger").is_dir()
+    with Path(os.environ["COMMAND_LOG"]).open("a") as stream:
+        stream.write(json.dumps(args) + "\\n")
+"""
+    for command in ("python", "gh"):
+        executable = binary_dir / command
+        executable.write_text(fake)
+        executable.chmod(0o755)
+    ledger_script = tmp_path / ".github" / "qualification-ledger.sh"
+    ledger_script.parent.mkdir()
+    ledger_script.write_text("#!/bin/bash\nset -e\nmkdir ledger\n")
+    ledger_script.chmod(0o755)
+    subprocess.run(
+        ["bash", "-e", "-o", "pipefail", "-c", step["run"]],
+        cwd=tmp_path,
+        env={
+            **os.environ,
+            "PATH": f"{binary_dir}{os.pathsep}{os.environ['PATH']}",
+            "RECOVERY_CANDIDATE": str(candidate_path),
+            "COMMAND_LOG": str(command_log),
+            "GITHUB_REPOSITORY": "PolicyEngine/policyengine.py",
+        },
+        check=True,
+        capture_output=True,
+        timeout=30,
+    )
+    assert [json.loads(line) for line in command_log.read_text().splitlines()] == [
+        [
+            "workflow",
+            "run",
+            "certify-uk-release.yaml",
+            "--repo",
+            "PolicyEngine/policyengine.py",
+            "--ref",
+            "main",
+            "-f",
+            "model_version=2.200.0",
+            "-f",
+            "data_version=1.58.9",
+            "-f",
+            "force=false",
+        ]
+    ]
+    assert job["permissions"]["actions"] == "write"
+    assert step["env"]["GH_TOKEN"] == "${{ github.token }}"
+
+
+@pytest.mark.parametrize("head_matches", [False, True])
+@pytest.mark.parametrize("verdict", ["qualified", "rejected", "accepted_review"])
+def test_refresh_checks_reruns_only_failed_pr_runs_for_the_qualified_head(
+    monkeypatch, tmp_path, head_matches, verdict
+):
+    cli = _cli()
+    receipt = make_receipt(
+        gate_results={
+            "qualified": gates(),
+            "rejected": gates(replay="fail"),
+            "accepted_review": gates(metrics_review="needs_review"),
+        }[verdict]
+    )
+    if verdict == "accepted_review":
+        receipt = accept_review(
+            receipt,
+            reviewer="reviewer",
+            accepted_receipt_sha256=receipt.unreviewed_sha256,
+            explanation="Expected change checked by the reviewer.",
+            accepted_at="2026-10-16T09:00:00Z",
+        )
+    branch = candidate_branch(receipt.pair)
+    head = receipt.harness.git_commit if head_matches else "f" * 40
+    monkeypatch.setattr(cli, "latest_receipts", lambda root, **_: [receipt])
+    monkeypatch.setattr(cli, "_open_candidate_branches", lambda: {branch: head})
+    runs = [
+        {
+            "id": identifier,
+            "head_sha": sha,
+            "event": event,
+            "status": status,
+            "conclusion": conclusion,
+        }
+        for identifier, sha, event, status, conclusion in (
+            (11, head, "pull_request", "completed", "failure"),
+            (12, head, "pull_request", "completed", "success"),
+            (13, head, "pull_request", "in_progress", "failure"),
+            (14, "a" * 40, "pull_request", "completed", "failure"),
+            (15, head, "push", "completed", "failure"),
+            (16, head, "pull_request", "completed", "timed_out"),
+        )
+    ]
+    calls = []
+
+    def fake_run(argv, **kwargs):
+        calls.append(argv)
+        if argv[:2] == ["gh", "api"]:
+            assert "--paginate" in argv
+            assert f"head_sha={receipt.harness.git_commit}" in argv[2]
+            assert "event=pull_request" in argv[2]
+            return SimpleNamespace(stdout="\n".join(json.dumps(run) for run in runs))
+        assert argv[:3] == ["gh", "run", "rerun"]
+        assert "--failed" in argv and kwargs["check"] is True
+        return SimpleNamespace(returncode=0)
+
+    monkeypatch.setattr(cli, "subprocess", SimpleNamespace(run=fake_run))
+    args = cli.parser().parse_args(["refresh-pr-checks", "--ledger", str(tmp_path)])
+    assert args.func(args) == 0
+    reruns = [call[3] for call in calls if call[:3] == ["gh", "run", "rerun"]]
+    if receipt.promotable and head_matches:
+        assert reruns == ["11", "16"]
+    else:
+        assert calls == []