Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/scripts/uk_replay_drain.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# Drain the UK replay qualification queue (see .github/workflows/uk-replay.yml).
#
# For each pending request, oldest first: mark it running, run it end to end
# (pipeline/uk_replay_qualify.py run-request), commit the receipt and results
# to the state branch, then write the terminal STATUS.json naming the
# receipt's digest and that commit. Stops when the queue is empty or the time
# budget is spent; the next scheduled run continues.
set -euo pipefail

STATE=state
CONTROL=(uv run --with rfc8785==0.1.4 --with jsonschema --with huggingface_hub python)
BUDGET_SECONDS=$((300 * 60))
started=$(date +%s)

now() { date -u +%Y-%m-%dT%H:%M:%SZ; }

push_state() {
git -C "$STATE" add -A
if git -C "$STATE" diff --cached --quiet; then
return 0
fi
git -C "$STATE" commit -qm "$1"
for attempt in 1 2 3 4 5; do
if git -C "$STATE" push -q origin "HEAD:$STATE_BRANCH"; then
return 0
fi
git -C "$STATE" pull -q --rebase origin "$STATE_BRANCH"
done
echo "could not push the state branch after five attempts" >&2
return 1
}

while :; do
git -C "$STATE" pull -q --rebase origin "$STATE_BRANCH"
request_id=$("${CONTROL[@]}" -m pipeline.uk_replay_queue pending --state "$STATE" --now "$(now)" | head -n 1)
if [ -z "$request_id" ]; then
echo "queue empty"
break
fi
if [ $(( $(date +%s) - started )) -gt "$BUDGET_SECONDS" ]; then
echo "time budget spent; the next scheduled run continues"
break
fi
echo "::group::request $request_id"
"${CONTROL[@]}" -m pipeline.uk_replay_queue set-status --state "$STATE" \
--request-id "$request_id" --status running --run-url "$RUN_URL" --now "$(now)"
push_state "Run $request_id"

work="$RUNNER_TEMP/requests/$request_id"
outcome=$("${CONTROL[@]}" -m pipeline.uk_replay_qualify run-request \
--request "$STATE/requests/$request_id/REQUEST.json" \
--state-dir "$STATE" --work "$work" --runner "$REPLAY_RUNNER" | tail -n 1)
status=${outcome%% *}
receipt_sha256=${outcome##* }
push_state "Receipt for $request_id: $status"
receipt_commit=$(git -C "$STATE" rev-parse HEAD)

"${CONTROL[@]}" -m pipeline.uk_replay_queue set-status --state "$STATE" \
--request-id "$request_id" --status "$status" --run-url "$RUN_URL" --now "$(now)" \
--receipt-sha256 "$receipt_sha256" --receipt-commit "$receipt_commit"
push_state "Status for $request_id: $status"
rm -rf "$work"
echo "::endgroup::"
done
109 changes: 109 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,41 @@ jobs:
- run: uv run --with pytest --with hypothesis --with pypdf --with pyyaml --with numpy --with psutil --with rfc8785==0.1.4 --with jsonschema pytest tests/ -q
env:
PYTHONPATH: .
# REPLAY NAMESPACES: every committed event replay (the legacy
# results/uk/events and each results/uk/replays/<variant>) is re-staged
# and re-compared from its committed artifacts, every committed release
# diff is re-rendered, and every sealed namespace must match its
# receipt byte for byte. Engine-free; the no-drift gate below fails on
# any regenerated byte that differs.
- name: Replay namespaces regenerate and seals hold
env:
PYTHONPATH: .
run: |
set -euo pipefail
run() { uv run --with rfc8785==0.1.4 --with jsonschema python "$@"; }
for root in results/uk/events results/uk/replays/*/; do
[ -d "$root" ] || continue
case "$root" in */diffs/) continue ;; esac
found=0
for manifest in "$root"/*/RUN_MANIFEST.json; do
[ -f "$manifest" ] || continue
found=1
dir=$(dirname "$manifest"); event=$(basename "$dir")
run pipeline/stage_uk_event.py --event "$event" --artifact-dir "$dir" > /dev/null
run pipeline/compare_uk_event.py --event "$event" --staged "$dir/STAGED.jsonl" --output-dir "$dir" > /dev/null
done
if [ "$found" = 1 ]; then
run pipeline/compare_uk_event.py --summary --events-root "$root" > /dev/null
echo "regenerated $root"
fi
done
for diff in results/uk/replays/diffs/*/; do
[ -f "$diff/DIFF.json" ] || continue
base=$(run -c "import json,sys; print(json.load(open(sys.argv[1]))['base']['root'])" "$diff/DIFF.json")
head=$(run -c "import json,sys; print(json.load(open(sys.argv[1]))['head']['root'])" "$diff/DIFF.json")
run -m pipeline.diff_uk_event_runs --base "$base" --head "$head" --output-dir "$diff" --check
done
run -m pipeline.uk_replay_qualify verify-sealed
- run: git diff --exit-code
- name: Upload built database
uses: actions/upload-artifact@v4
Expand All @@ -52,6 +87,80 @@ jobs:
path: data/scorecard.db
retention-days: 30

replay-engine:
# The event replay against each certified variant's own engine: the
# exact environment from the variant's requirements freeze, the
# construction guards, and the committed premise and diagnostic files
# regenerated byte for byte. With the HUGGING_FACE_TOKEN secret it also
# replays one real measure-year on the variant's certified dataset and
# compares the artifact with the committed bytes (replay determinism);
# without the secret (forks) that step announces it did not run.
name: replay engine · ${{ matrix.variant }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
variant:
- pe5.0.2_uk2.89.2_populace-uk-2023-dd68c73-4aa4b14-20260619T023711Z
- pe6.2.6_uk2.125.1_policyengine-uk-data-1.58.0
env:
PYTHONPATH: .
VARIANT: ${{ matrix.variant }}
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- name: Install the variant's exact environment
run: |
set -euo pipefail
field() { python3 -c "import json,sys; d=json.load(open('data/uk/replay_variants.json')); v=[v for v in d['variants'] if v['variant_id']==sys.argv[1]][0]; print(eval(sys.argv[2], {'v': v}))" "$VARIANT" "$1"; }
echo "REQUIREMENTS=$(field "v['requirements']['path']")" >> "$GITHUB_ENV"
echo "ENGINE=$(field "v['identity']['packages']['policyengine-uk']['version']")" >> "$GITHUB_ENV"
echo "RESULT_ROOT=$(field "v['result_root']")" >> "$GITHUB_ENV"
echo "LEGACY=$(field "v['layout'] == 'legacy_events'")" >> "$GITHUB_ENV"
- run: |
uv venv --python 3.12 .venv-variant
uv pip install --python .venv-variant/bin/python -r "$REQUIREMENTS"
uv pip install --python .venv-variant/bin/python rfc8785==0.1.4 jsonschema
- name: The installed environment is exactly the variant's
run: .venv-variant/bin/python -c "from pipeline import uk_replay_variants as rv; rv.verify_installed(rv.get_variant('$VARIANT')); print('installed environment matches $VARIANT')"
- name: Construction guards on the variant's engine
run: |
set -euo pipefail
for event in autumn_budget_2024 autumn_statement_2023 spring_budget_2024 spring_statement_2025 spring_budget_2023; do
.venv-variant/bin/python -m pipeline.compute_uk_event --event "$event" --variant "$VARIANT" --guards > /dev/null
echo "guards hold: $event"
done
- name: Construction premises regenerate
run: .venv-variant/bin/python -m pipeline.audit_uk_event_premises --all --expect-engine "$ENGINE" --output "$RESULT_ROOT/CONSTRUCTION_PREMISES.json" --check
- name: Re-verified diagnostics regenerate
if: env.LEGACY == 'False'
run: |
set -euo pipefail
.venv-variant/bin/python pipeline/diagnose_uk_event_models.py --reverify results/uk/events/MODEL_DIAGNOSTICS.json --expect-engine "$ENGINE" --output "$RUNNER_TEMP/MODEL_DIAGNOSTICS.json"
cmp "$RUNNER_TEMP/MODEL_DIAGNOSTICS.json" "$RESULT_ROOT/MODEL_DIAGNOSTICS.json"
- name: One real measure-year replays byte for byte
if: env.LEGACY == 'False'
env:
HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }}
run: |
set -euo pipefail
if [ -z "${HUGGING_FACE_TOKEN:-}" ]; then
echo "::notice::HUGGING_FACE_TOKEN not available (fork PR or unset secret): the real replay determinism check did NOT run"
exit 0
fi
uv run --with huggingface_hub python - <<'PY'
import json, os
from huggingface_hub import hf_hub_download
d = json.load(open("data/uk/replay_variants.json"))
v = [v for v in d["variants"] if v["variant_id"] == os.environ["VARIANT"]][0]["identity"]["dataset"]
print(hf_hub_download(repo_id=v["repo_id"], repo_type=v["repo_type"], filename=v["path"], revision=v["revision"], token=os.environ["HUGGING_FACE_TOKEN"]))
PY
key=autumn_budget_2024__private_school_vat_20pct
.venv-variant/bin/python -m pipeline.compute_uk_event --event autumn_budget_2024 --variant "$VARIANT" \
--measures "$key" --years 2026 --workers 1 --output-dir "$RUNNER_TEMP/determinism"
cmp "$RUNNER_TEMP/determinism/${key}_2026.json" "$RESULT_ROOT/autumn_budget_2024/${key}_2026.json"
echo "replayed $key 2026 byte-identical to the committed artifact"

app:
name: typecheck · build
runs-on: ubuntu-latest
Expand Down
127 changes: 127 additions & 0 deletions .github/workflows/uk-replay.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
name: UK replay qualification

# WHY THIS EXISTS
#
# Release qualification in policyengine.py (W4) asks this repository to rerun
# the OBR fiscal-event replay corpus on a candidate model/data pair before it
# is promoted, and to return an artifact-bound receipt. The interface is
# docs/uk_replay/QUALIFICATION_INTERFACE.md; the run is
# pipeline/uk_replay_qualify.py.
#
# GitHub keeps at most one pending run per concurrency group and cancels the
# older pending ones, so a request is first written to the persistent queue
# on the `replay-state` branch (enqueue job, no concurrency group). The drain
# job then runs queued requests one at a time. A scheduled run drains anything
# a cancelled run left behind. A request whose run died mid-way is retried
# after the stale bound in pipeline/uk_replay_queue.py.
#
# Simulations: one drain job at a time, at most two workers inside it, so
# never more than two simulations at once from this workflow.
#
# Secrets: HUGGING_FACE_TOKEN reads the certified datasets. Without it a
# request is rejected with data_access_failed rather than skipped.

on:
repository_dispatch:
types: [uk-replay-qualification-request]
workflow_dispatch:
inputs:
request:
description: "A qualification request (JSON). Empty: drain the queue only."
required: false
default: ""
schedule:
- cron: "41 */3 * * *"

permissions:
contents: write

env:
STATE_BRANCH: replay-state

jobs:
enqueue:
name: record the request
if: github.event_name == 'repository_dispatch' || (github.event_name == 'workflow_dispatch' && github.event.inputs.request != '')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- name: Check out the state branch
run: |
set -euo pipefail
if git ls-remote --exit-code --heads origin "$STATE_BRANCH" >/dev/null; then
git fetch -q origin "$STATE_BRANCH"
git worktree add -q state "origin/$STATE_BRANCH"
git -C state switch -q -C "$STATE_BRANCH"
else
git worktree add -q --orphan -b "$STATE_BRANCH" state
fi
git -C state config user.name "github-actions[bot]"
git -C state config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Write the request
env:
DISPATCH_REQUEST: ${{ toJSON(github.event.client_payload.request) }}
MANUAL_REQUEST: ${{ github.event.inputs.request }}
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "repository_dispatch" ]; then
printf '%s' "$DISPATCH_REQUEST" > "$RUNNER_TEMP/request.json"
else
printf '%s' "$MANUAL_REQUEST" > "$RUNNER_TEMP/request.json"
fi
- name: Enqueue and push
env:
PYTHONPATH: .
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
uv run --with rfc8785==0.1.4 --with jsonschema python -m pipeline.uk_replay_queue \
enqueue --state state --request "$RUNNER_TEMP/request.json" --run-url "$RUN_URL" --now "$now" \
| tee "$RUNNER_TEMP/enqueued.txt"
git -C state add -A
if git -C state diff --cached --quiet; then
echo "already recorded: $(cat "$RUNNER_TEMP/enqueued.txt")"
exit 0
fi
git -C state commit -qm "Queue $(cut -d' ' -f1 "$RUNNER_TEMP/enqueued.txt")"
for attempt in 1 2 3 4 5; do
git -C state push -q origin "HEAD:$STATE_BRANCH" && exit 0
git -C state pull -q --rebase origin "$STATE_BRANCH"
done
exit 1

drain:
name: run queued requests
needs: [enqueue]
if: always() && (needs.enqueue.result == 'success' || needs.enqueue.result == 'skipped')
runs-on: ubuntu-latest
timeout-minutes: 340
concurrency:
group: uk-replay-drain
cancel-in-progress: false
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- name: Check out the state branch
run: |
set -euo pipefail
if ! git ls-remote --exit-code --heads origin "$STATE_BRANCH" >/dev/null; then
echo "no state branch yet; nothing queued"
echo "EMPTY=1" >> "$GITHUB_ENV"
exit 0
fi
git fetch -q origin "$STATE_BRANCH"
git worktree add -q state "origin/$STATE_BRANCH"
git -C state switch -q -C "$STATE_BRANCH"
git -C state config user.name "github-actions[bot]"
git -C state config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Drain the queue
if: env.EMPTY != '1'
env:
PYTHONPATH: .
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
HUGGING_FACE_TOKEN: ${{ secrets.HUGGING_FACE_TOKEN }}
REPLAY_RUNNER: github-actions ${{ runner.os }} ${{ runner.arch }}
run: bash .github/scripts/uk_replay_drain.sh
18 changes: 10 additions & 8 deletions data/uk/replay_variants.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,11 @@
"packaged_bundle_manifest_sha256": "255dbb893f455d078a6bcece87399f0a22478f56bc4c6e526b758c4fd5b4db24",
"packages": {
"policyengine": {
"record_sha256": "5d5de97a40dbc525bc41a6347fc793ba772bd78f9e0630f2f1d382bc92575e68",
"record_sha256": "8d5eec548a5ce206a8fe59ef018542c701e26c0f927351f92339a7a57591b990",
"version": "5.0.2"
},
"policyengine-core": {
"record_sha256": "dd6570fe988159d89e45df66ccc43355f70abddb8a3a58d246ef38ce711e5687",
"record_sha256": "103128a1c2a3397268b65e33b83edddfa8e406a4eb64fb6f4ff2310db421c770",
"version": "3.27.1"
},
"policyengine-uk": {
Expand All @@ -83,9 +83,10 @@
"model_package": "policyengine-uk",
"model_version": "2.89.2",
"policyengine_version": "5.0.2"
}
},
"requirements_sha256": "450e64b173b66333027ac36e056e19928d23df1cbe2690e462bab09b31ddff9d"
},
"identity_sha256": "5c642304a4fff8f30074899b2bb530197920703add1b92304a4c3c3a48c5d176",
"identity_sha256": "3f1a33f31ac70a8f0beac2373b6408c8be3366128ec0d0e3c1b716de3880003c",
"layout": "legacy_events",
"note": "The bundle the track 1 replays were computed against. Its 1.3GB populace dataset needs Modal's 32GiB containers for full events.",
"requirements": {
Expand Down Expand Up @@ -145,12 +146,12 @@
"packaged_bundle_manifest_sha256": "8633ad814c9c85e53c7c513f69c4c7b1500a03cd2f6ef133d6e2ea8346b2db54",
"packages": {
"policyengine": {
"record_sha256": "072606d8f554123484caff5a1e0f12c3203b96d03f5990ebc4d80d6491441fd0",
"record_sha256": "f5990ac205b959621c56585c16ed963a5d1b07b1403adaa7242e0131fbf61f40",
"version": "6.2.6",
"wheel_sha256": "9ac20a2599cc1f4b50c128c2245042b105c40592e07fc25d39c99a529c9f410b"
},
"policyengine-core": {
"record_sha256": "7a4e8a82c3467681971f5160428296b06e65357b8a70801f2cd631ea0943394a",
"record_sha256": "53a92f2e67982a16faeba61b8b3d145d3ab9a50d298e92c512d61166ffe5d5d4",
"version": "3.33.0",
"wheel_sha256": "2e32d02a8910ab683a2b7600987904b351d058fc2bd5910a8ea6641438bcd789"
},
Expand Down Expand Up @@ -178,9 +179,10 @@
"model_package": "policyengine-uk",
"model_version": "2.125.1",
"policyengine_version": "6.2.6"
}
},
"requirements_sha256": "5c333faef091e6780b2c178ac7584249ac7db0159841921727b913505577bcdb"
},
"identity_sha256": "f241374dea3ca00aeba8849437481da34e6c6c9ddb0bb9b75d3e72bbbe22fd69",
"identity_sha256": "3f62a208a9bf522cccb391fe65c1411124abaf4488ab760c522b0568f5b417aa",
"layout": "replay_namespace",
"note": "The newest certified pair on 11 October 2026; data built with policyengine-uk 2.122.2 and certified for 2.125.1.",
"requirements": {
Expand Down
6 changes: 4 additions & 2 deletions docs/uk_replay/QUALIFICATION_INTERFACE.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,10 @@ Two flows use the same receiver:

* `request_id` is not a field: it is the SHA-256 of the request's JCS
bytes. Both sides test against the golden fixture.
* `requested_by` accepts additional provenance fields (for example
`run_attempt`); every other object is closed.
* `requested_by` names the requesting repository and commit;
`workflow_run_id` is optional (a scorecard catch-up run started outside
Actions has none), and additional provenance fields such as `run_attempt`
are accepted. Every other object is closed.
* The wrapper is identified by `version` plus either a PyPI wheel
(`{"pypi": {"wheel_sha256": …}}`) or, for an unreleased candidate whose
regenerated manifest is not on PyPI yet, a public git commit. Scorecard
Expand Down
7 changes: 7 additions & 0 deletions docs/uk_replay/RECIPE.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,13 @@ PYTHONPATH=. .venv-candidate/bin/python -m pipeline.compute_uk_event --event aut
PYTHONPATH=. .venv-candidate/bin/python pipeline/stage_uk_event.py --event autumn_budget_2024 --artifact-dir results/uk/replays/pe6.2.6_uk2.125.1_policyengine-uk-data-1.58.0/autumn_budget_2024
```

On the shared Mac, check the host reservation before a local run:
`python3 ~/reviews/fleet-ops/tools/reservation.py allowed w6-replay` exits 1
while a reserved run (the 29 October–2 November triple-lock rebuild) holds
heavy local work back. Qualification runs from policyengine.py go through
the receiver workflow on GitHub runners instead; see
[RELEASE_REPLAY.md](RELEASE_REPLAY.md).

The dataset must already be local: the runner finds the certified bytes by
SHA-256 in the Hugging Face cache, or takes `--dataset-file <path>`, and
copies them into `.venv-replay-checks/runtime/<variant_id>/data/`. Results
Expand Down
Loading
Loading