# v0.3.0 candidate release tracker and assurance runbook

- Status: **approval-neutral candidate-byte baseline; authoritative release
  status is external and digest-bound**
- Audience: maintainers, reviewers and the project owner
- Research cut-off: **29 July 2026**

This is the dependency-ordered operating record for the v0.3.0 candidate. It
is not a review receipt, approval or release record. Candidate bytes preserve
a `not_run` baseline and do not self-assert their current gate state. Only
version-scoped evidence bound to the final candidate identity records a gate
result, owner decision or release and deployment authority.

The final v0.2.0 receipts and release metadata are historical. They remain
useful regression evidence, but cannot approve changed v0.3.0 bytes.
The owner selected v0.3.0 dated 11 August 2026 for `CITATION.cff` and the
changelog before the candidate freeze. Both are governed inputs and therefore
cannot be changed after that freeze. They identify the intended version but do
not confer approval: the exact current G1–G9 state and release authority are
recorded only in subsequent version-scoped, digest-bound evidence. A later
input edit would be a new governed candidate, not a byte-identical promotion.

## Candidate identity

| Item | Current value |
|---|---|
| Product version | `0.3.0` (approval-neutral candidate bytes) |
| Candidate Git commit | Bind the frozen value in external validation evidence |
| Bundle release root | Read the frozen value from `bundle/CHECKSUMS.sha256` and bind it in external evidence |
| Source snapshot | `hmlr-public-metadata-v0.2.0` |
| Frozen snapshot directory | `source/snapshots/2026-07-29T091915Z` |
| Source input manifest | `source/input-manifest-v0.2.0.json` |
| Source model version | `SOURCE_MODEL_VERSION = "0.2.0"` |
| Pinned Explorer | `v0.6.2`, commit `9430b3931f96bd9e6e06165c15b522742611f3e9`, annotated tag object `43e53f36d869ba7ca2420990191a0834a969dcd2`, immutable release `368773937` |
| Public release candidate | Record its identity and authority in version-scoped external evidence |
| Public URL verification | Record the served identity and journey result in version-scoped external evidence |

The v0.2.0 labels in the snapshot, input manifest and source-model constant are
intentional source identities. They are not stale product-version labels and
must not be migrated with the v0.3.0 product candidate.

## Gate tracker

Every candidate-byte row has the `not_run` baseline shown below. This table is
not a live status board: evidence for the same frozen commit and bundle root is
the sole authority for the current state. “Pending”, a historical pass and an
unbound test result are not passes.

| Land Registry gate | Required result for the v0.3.0 candidate | Candidate-byte baseline |
|---|---|---|
| G1 — discovery profile | Profile pack validates, rehashes and is independently reviewed for the candidate | `not_run` |
| G2 — source snapshot | Frozen envelopes, outcomes, coverage and input identities rehash | `not_run` |
| G3 — rights, privacy and safety | Fresh exact-candidate review finds no prohibited content, credentials or unsafe rights claims | `not_run` |
| G4 — OKF and data integrity | Full schemas, references, semantic assertions, checksums and pinned-consumer checks pass | `not_run` |
| G5 — evaluation | The 24-question suite, forbidden targets, caveats and search journeys pass fresh independent review | `not_run` |
| G6 — user-facing quality | Authored-site and pinned-Explorer journeys, accessibility, security and performance evidence pass | `not_run` |
| G7 — reproducibility | Two clean builds are byte-identical and the complete diff matches the dependency graph | `not_run` |
| G8 — package integrity | Frozen archive, manifest, provenance, checksums and deployment plan bind the same bytes | `not_run` |
| G9 — owner approval | A separately digest-bound independent review covers the exact pre-G9 evidence, then the owner explicitly approves that review, commit, root, governed release coordinates, claims and residual risks | `not_run`; consult external evidence for the authoritative state |

Official CPSV-AP SHACL validation and any materialised-inference assessment
are also `not_run`. The candidate must not claim that either has passed.

The source-field evidence, CPSV adversarial-binding and URL-hardening
corrections identified by independent implementation review are present and
covered by local regression tests. This is readiness for exact-candidate
review, not gate evidence. Candidate bytes deliberately retain `not_run` and
`release_ready: false`; subsequent digest-bound evidence is authoritative for
the actual G1–G9 and release state.

## Dependency-ordered runbook

Run these steps only after reviewing the complete authored diff. The build is
offline; local G1–G8 checks and browser journeys do not require a GitHub token.
Every Bash block is fail-fast. Run a complete block in one Bash process; do not
copy only the mutating command at its end. `OKF_REMOTE` is deliberately required
instead of defaulted: the transition checker requires exactly one credential-free
fetch destination and one credential-free push destination, both bound to
`github.com/chris-page-gov/okf-LandRegistry`, without printing either URL.

The governed build and metadata producer require exactly CPython 3.12.11 in
`.venv`, isolated mode, disabled bytecode writes and a fresh, external, empty
`0700` cache namespace. Create the environment with `--without-pip`, then use
an external installer with `--no-compile --require-hashes`; pip, `.pth` files,
startup customisers, bytecode and files outside the locked distribution
inventories are prohibited in the target. Every `runtime_cache` below must be a
new absolute directory and must not be reused for another command.

### 1. Validate authored contracts

```bash
set -euo pipefail
.venv/bin/python -B scripts/check_domain_profile.py \
  domain-profile/domain-profile.json \
  --equivalent domain-profile/domain-profile.yaml
.venv/bin/python -B -m unittest \
  tests.test_domain_profile \
  tests.test_explorer_contract \
  tests.test_evaluate -v
```

Confirm that the consumer lock still names exactly Explorer v0.6.2 commit
`9430b3931f96bd9e6e06165c15b522742611f3e9`, annotated tag object
`43e53f36d869ba7ca2420990191a0834a969dcd2` and immutable release
`368773937`. Confirm that it binds all six acceptance executable materials
and the governed `queryPolicy.ts` source.
Bundle Wiki v1 remains independently frozen at Explorer v0.6.0, while
Predicate Registry v2 is pinned to its published v0.6.1 profile lock and
schema bytes. The build digest-binds both
v0.2.0 journey manifests as immutable historical regression inputs. Their
product and search SHA-256 values are respectively
`f721e6c9915484d72ac38b3630926a0461ff7fe417f39651a1a8e79546968431` and
`b00936fcaee53cd481e307c8bf9279416b0d80ae91fefcba2d22b4591d709753`;
`tests.test_explorer_contract` pins them and compares their journey bodies
with the v0.3.0 baselines. They are not current execution manifests. Confirm
that the two v0.3.0 manifests named below are the only manifests selected for
current candidate execution; never use a v0.2.0 manifest or receipt as v0.3.0
execution evidence.

### 2. Build once from the named frozen snapshot

```bash
set -euo pipefail
git add -A -- . \
  ':(exclude,top)bundle/**' \
  ':(exclude,top)validation/**' \
  ':(exclude,top)dist/**'
git diff --cached --check
stage_check_cache="$(mktemp -d "${TMPDIR:-/tmp}/okf-python-cache.XXXXXX")"
chmod 700 "$stage_check_cache"
.venv/bin/python -I -B -X "pycache_prefix=$stage_check_cache" \
  scripts/check_release_transition.py staged-candidate
runtime_lock="${TMPDIR:-/tmp}/okf-landregistry-build.lock"
if ! mkdir -m 700 "$runtime_lock"; then
  echo "A governed Land Registry build already holds $runtime_lock" >&2
  exit 1
fi
cleanup_runtime_lock() { rmdir "$runtime_lock"; }
trap cleanup_runtime_lock EXIT INT TERM
test -d .venv
test ! -L .venv
test -x .venv/bin/python
if find .venv -type f \( -name '*.pth' -o -name '*.py[co]' -o -name sitecustomize.py -o -name usercustomize.py \) -print -quit | grep -q .; then
  echo 'The pre-invocation runtime contains a startup hook or bytecode file' >&2
  exit 1
fi
repository_parent="$(cd .. && pwd -P)"
recovery_parent="$(mktemp -d "${repository_parent}/okf-landregistry-build-1-recovery.XXXXXX")"
previous_output="${recovery_parent}/previous-bundle"
runtime_cache="$(mktemp -d "${TMPDIR:-/tmp}/okf-python-cache.XXXXXX")"
chmod 700 "$runtime_cache"
test ! -L "$runtime_cache"
test -z "$(find "$runtime_cache" -mindepth 1 -print -quit)"
.venv/bin/python -I -B -X "pycache_prefix=$runtime_cache" scripts/build.py \
  --snapshot-dir source/snapshots/2026-07-29T091915Z \
  --publication-base https://chris-page-gov.github.io/okf-LandRegistry/ \
  --replace \
  --previous-output "$previous_output"
test -d "$previous_output"
rmdir "$runtime_lock"
trap - EXIT INT TERM
git add -A -- bundle
.venv/bin/python -B scripts/check_release_evidence.py --staged-candidate
test_runtime_cache="$(mktemp -d "${TMPDIR:-/tmp}/okf-python-cache.XXXXXX")"
chmod 700 "$test_runtime_cache"
.venv/bin/python -E -s -B -X "pycache_prefix=$test_runtime_cache" \
  -m unittest discover -s tests -v
.venv/bin/python -B scripts/check_okf.py
```

The first `git add` is the causal freeze: it stages every authored input while
leaving the generated bundle and later evidence surfaces untouched. The builder
then consumes and rechecks that exact stage-zero index. Only after the build is
the generated bundle staged and the complete staged candidate validated. A
failure before that validation is not a candidate and must not be committed.

The builder constructs the candidate at the exact, initially absent
`previous_output` path outside the repository. Its parent is a sibling of the
checkout, which keeps the swap slot on the same file system without adding an
untracked path to the candidate. One atomic directory exchange makes the new
candidate live and leaves the complete pre-build bundle at that exact recovery
path. The builder never deletes, prunes or overwrites it. Record the path with
the build log; moving or deleting it is a separate owner-authorised operation.
If the build or final input check fails, the live bundle remains intact and the
swap slot is retained for diagnosis.

The in-process observer starts after Python startup and initial imports; it
verifies the runtime before release work continues but does not attest the
exact source bytes already executed. The executable pre-invocation check
rejects `.pth`, bytecode and customiser hooks and holds a cooperating
single-writer lock for the build. This is a fail-closed operational mitigation,
not cryptographic proof against an uncooperative concurrent mutator. A pre-site
staged launcher would be needed to close that remaining gap and is proposed for
a future contract revision.

Run the deterministic calibration diagnostic. It is a regression result, not
independent G5 acceptance:

```bash
set -euo pipefail
.venv/bin/python -B scripts/evaluate.py \
  --output validation/candidate-v0.3.0/evidence/evaluation-diagnostic.json \
  --k 10 \
  --min-expected-source-success-at-k 1.0 \
  --min-expected-target-recall-at-k 0.90 \
  --min-all-expected-target-success-at-k 1.0 \
  --min-mrr 0.80
```

### 3. Freeze and reconcile the candidate

The complete candidate is already staged by step 2 while all diagnostic,
review and archive material remains outside it. The staged-candidate check compares the index
with the governed inventory in the staged build receipt, requires every input
to be a regular stage-zero Git blob with the exact byte count and digest,
rejects conflicts and partial staging, and refuses staged `validation/**` or
`dist/**` paths:

```bash
set -euo pipefail
OKF_REMOTE="${OKF_REMOTE:?set the verified canonical GitHub remote name}"
OKF_CANDIDATE_BRANCH="candidate/v0.3.0"
.venv/bin/python -B scripts/check_release_transition.py \
  remote-binding --remote "$OKF_REMOTE"
.venv/bin/python -B scripts/check_release_transition.py staged-candidate
.venv/bin/python -B scripts/check_release_evidence.py --staged-candidate
git diff --cached --check
git commit -m "Migrate Land Registry to OKF v0.3 semantic candidate"
CANDIDATE_SHA="$(git rev-parse HEAD)"
.venv/bin/python -B scripts/check_release_evidence.py \
  --candidate-only \
  --candidate-commit-sha "$CANDIDATE_SHA"
```

Do not stage `validation/candidate-v0.3.0/evidence/evaluation-diagnostic.json`
in this commit. It belongs in a later evidence-only descendant. After the
intended candidate is committed, record its full 40-character Git SHA and the
complete release root from `bundle/CHECKSUMS.sha256` in the candidate-bound
validation evidence. Do not write the commit SHA back into a file governed by
that commit: doing so would create an impossible self-reference and a
different commit. Use a second, separate clean checkout for the reproducibility
build. Its generated bundle must remain byte-identical to the committed build
1 candidate:

```bash
set -euo pipefail
OKF_REMOTE="${OKF_REMOTE:?set the verified canonical GitHub remote name}"
OKF_CANDIDATE_BRANCH="candidate/v0.3.0"
OKF_REPRO_CHECKOUT="${OKF_REPRO_CHECKOUT:?set an absent absolute path for the clean reproducibility checkout}"
OKF_BASE_PYTHON="${OKF_BASE_PYTHON:?set the absolute CPython 3.12.11 executable}"
CANDIDATE_SHA="$(git rev-parse HEAD)"
case "$OKF_REPRO_CHECKOUT" in
  /*) ;;
  *) echo 'OKF_REPRO_CHECKOUT must be absolute' >&2; exit 1 ;;
esac
test ! -e "$OKF_REPRO_CHECKOUT"
CANDIDATE_CHECKOUT="$(pwd -P)"
git worktree add --detach "$OKF_REPRO_CHECKOUT" "$CANDIDATE_SHA"
cd "$OKF_REPRO_CHECKOUT"
test -z "$(git status --porcelain --untracked-files=all)"
"$OKF_BASE_PYTHON" -I -B -c \
  'import platform; assert platform.python_implementation() == "CPython" and platform.python_version() == "3.12.11"'
"$OKF_BASE_PYTHON" -B -m venv --without-pip .venv
"$OKF_BASE_PYTHON" -B -m pip --python .venv install \
  --no-compile --require-hashes -r requirements-lock.txt
runtime_lock="${TMPDIR:-/tmp}/okf-landregistry-build.lock"
if ! mkdir -m 700 "$runtime_lock"; then
  echo "A governed Land Registry build already holds $runtime_lock" >&2
  exit 1
fi
cleanup_runtime_lock() { rmdir "$runtime_lock"; }
trap cleanup_runtime_lock EXIT INT TERM
test -d .venv
test ! -L .venv
test -x .venv/bin/python
if find .venv -type f \( -name '*.pth' -o -name '*.py[co]' -o -name sitecustomize.py -o -name usercustomize.py \) -print -quit | grep -q .; then
  echo 'The pre-invocation runtime contains a startup hook or bytecode file' >&2
  exit 1
fi
repository_parent="$(cd .. && pwd -P)"
recovery_parent="$(mktemp -d "${repository_parent}/okf-landregistry-build-2-recovery.XXXXXX")"
previous_output="${recovery_parent}/previous-bundle"
runtime_cache="$(mktemp -d "${TMPDIR:-/tmp}/okf-python-cache.XXXXXX")"
chmod 700 "$runtime_cache"
test ! -L "$runtime_cache"
test -z "$(find "$runtime_cache" -mindepth 1 -print -quit)"
.venv/bin/python -I -B -X "pycache_prefix=$runtime_cache" scripts/build.py \
  --snapshot-dir source/snapshots/2026-07-29T091915Z \
  --publication-base https://chris-page-gov.github.io/okf-LandRegistry/ \
  --replace \
  --previous-output "$previous_output"
test -d "$previous_output"
rmdir "$runtime_lock"
trap - EXIT INT TERM
git diff --exit-code -- bundle
.venv/bin/python -B scripts/check_release_transition.py \
  bundle-inventory --bundle bundle
.venv/bin/python -B scripts/change_impact.py \
  --base v0.2.0 \
  --head "$CANDIDATE_SHA" \
  --check
.venv/bin/python -B scripts/check_release_evidence.py \
  --candidate-only \
  --candidate-commit-sha "$CANDIDATE_SHA"
cd "$CANDIDATE_CHECKOUT"
.venv/bin/python -B scripts/check_release_transition.py \
  remote-binding --remote "$OKF_REMOTE"
git push "$OKF_REMOTE" "$CANDIDATE_SHA:refs/heads/$OKF_CANDIDATE_BRANCH"
```

Never reuse the first build's swap slot. After the first build it contains the
pre-build bundle; after the clean-checkout second build the distinct second
slot contains the committed build 1 candidate and that checkout's live
`bundle/` contains build 2. Concrete recovery paths are operational results
only: the deterministic build receipt contains the stable placeholder
`<owner-selected-empty-same-filesystem-path>`, so different paths cannot change
the governed bundle bytes.

Any unexpected file—including a Git-ignored file—missing file, non-regular
path, digest mismatch or candidate identity change stops the run. Run build 2
only in the clean detached checkout and compare every governed byte before
recording G7 evidence. Retain that checkout and both recovery paths through
release verification; removing them is a later cleanup action.

The transition check binds each file's device, inode, type, size and change
times before and after reading, then repeats the complete ignored-inclusive
inventory. The following Pages upload is a separate action and therefore
depends on the GitHub-hosted job remaining a sequential, single-writer
workspace with no untrusted background mutator between those adjacent steps.
That trust boundary is residual deployment risk, not permission to run other
processes concurrently with release publication.

Every later commit may add only files below
`validation/candidate-v0.3.0/` or the two exact archives
`dist/okf-landregistry-0.3.0-candidate-a.zip` and
`dist/okf-landregistry-0.3.0-candidate-b.zip`. The NUL-safe staged-evidence
check below enforces that surface, batch-checks Git blob sizes, applies per-file
and aggregate ceilings, and rejects non-printable, non-NFC, hidden, temporary,
modified, deleted, renamed, conflicted, symbolic-link and non-stage-zero paths.
After each such
commit, rerun `--candidate-only` against `$CANDIDATE_SHA`. Never squash,
rebase or amend the candidate after evidence is bound: those operations lose
its identity. Do not merge a branch based before the candidate, because its
additional parent introduces the candidate tree after that parent and the
history check correctly fails closed.

### 4. Run the exact Explorer twice

Use a clean checkout of the pinned Explorer commit and its locked dependency
tree. Run the product and calibration manifests separately:

```bash
set -euo pipefail
pnpm --dir apps/okf-explorer acceptance:bundle -- \
  --bundle-root <absolute-path-to-this-repository>/bundle \
  --journeys <absolute-path-to-this-repository>/evaluation/explorer-v0.3.0-journeys.json \
  --output <absolute-external-evidence-directory>/explorer-product-v0.3.0-candidate.json

pnpm --dir apps/okf-explorer acceptance:bundle -- \
  --bundle-root <absolute-path-to-this-repository>/bundle \
  --journeys <absolute-path-to-this-repository>/evaluation/explorer-search-calibration-v0.3.0.json \
  --output <absolute-external-evidence-directory>/explorer-search-v0.3.0-candidate.json
```

Receipts must record a clean Explorer source tree, the exact wrapper,
invocation-lock module, complete six-file acceptance executable set,
executable-build identity, journey-manifest digest and complete candidate
bundle tree.
Do not carry forward a v0.2.0 receipt. Keep fresh receipts outside the frozen
candidate checkout during execution so its exact clean Git state remains
verifiable. They may be copied into a later evidence-only commit after their
candidate bindings and digests have been checked; governed inputs and bundle
bytes must not change.

### 4a. Reproduce the authored-site G6 checks

First run the deterministic authored-site, consumer-contract and workflow
checks:

```bash
set -euo pipefail
.venv/bin/python -B -m unittest \
  tests.test_authored_site_browser_quality_runner \
  tests.test_pages \
  tests.test_explorer_contract \
  tests.test_workflow -v
```

Run the maintained authored-site observer against the clean, frozen candidate
and clean pinned Explorer checkout. The runner creates and closes its own
loopback-only server:

```bash
set -euo pipefail
node scripts/run_authored_site_browser_quality.mjs \
  --repository-root <absolute-path-to-this-repository> \
  --bundle-root <absolute-path-to-this-repository>/bundle \
  --explorer-checkout <absolute-path-to-clean-explorer-v0.6.2-checkout> \
  --candidate-commit <candidate-commit-sha> \
  --release-root <bundle-release-root-sha256> \
  --output <absolute-external-evidence-directory>/authored-site-browser-quality-v0.3.0-candidate.json
```

The runner requires absolute paths, verifies the exact clean commit, complete
bundle checksum file set and release root, build-receipt consumer-lock digest,
every pinned Explorer source/build/profile material and the exact Playwright
1.62.1 and axe-core 4.12.1 installations. It tests `/`,
`/accessibility.html`, `/catalogue-index.html` and `/404.html` through a bounded
desktop/mobile, JavaScript/no-JavaScript, 200%/400% effective-width,
forced-colours and reduced-motion matrix. The structured observations cover:

- first-tab skip-link operation; keyboard-only search, filter, result/detail
  traversal and official-source navigation;
- 200% and 400% zoom, 320-pixel reflow, forced colours and reduced motion;
- an axe scan with zero critical or serious violations, with every incomplete
  result investigated rather than silently discarded;
- zero console, page and resource failures; zero cookies, local storage and
  session storage; no unsafe DOM sink, credential or signed URL; and no
  unexpected external request; and
- raw and gzip HTML size, request and transferred-byte totals, time to first
  usable interaction, and the files fetched for overview-first and lazy-shard
  journeys.

Any axe `incomplete` item remains a blocker until it is examined. If the first
run reports such items, retain that diagnostic receipt, create an exact
candidate-bound `okf-hmlr-axe-incomplete-review.v1` input covering every
fingerprint, and rerun to a different new output path with:

```bash
set -euo pipefail
node scripts/run_authored_site_browser_quality.mjs \
  --repository-root <absolute-path-to-this-repository> \
  --bundle-root <absolute-path-to-this-repository>/bundle \
  --explorer-checkout <absolute-path-to-clean-explorer-v0.6.2-checkout> \
  --candidate-commit <candidate-commit-sha> \
  --release-root <bundle-release-root-sha256> \
  --incomplete-review <absolute-path-to-reviewed-incomplete-input> \
  --output <absolute-external-evidence-directory>/authored-site-browser-quality-v0.3.0-reviewed.json
```

For Explorer relationship hydration, compare each fetched JSON or gzip member
with the pinned limits in `contracts/okf-explorer.consumer-lock.json`. Record
initial shell size, first interaction, maximum loaded JSON bytes, incident
route chunk counts/rows/compressed bytes and total bundle size as separate
measurements; do not disguise whole-bundle size as initial transfer or invent
an ungoverned threshold. The two locked-Explorer receipts from step 4 remain
required alongside this authored-site receipt.

An independent reviewer must decide all four G6 checks named by the release
evidence contract: `automated-journeys`,
`manual-accessibility-journeys`, `security-critical-zero` and
`performance-budgets`. AI-assisted inspection is not a human accessibility
audit or a WCAG conformance claim. The locked Explorer toolchain uses
Playwright 1.62.1 and axe-core 4.12.1.
The maintained runner records automated candidate observations only. Its
terminal success deliberately says neither `pass` nor G6 approval, and it does
not perform the independent decision, human accessibility audit,
representative-user study, assistive-technology assessment or WCAG conformance
assessment. The candidate-byte G6 field retains the `not_run` baseline; only
separately reviewed, digest-bound evidence with both pinned-Explorer receipts
records the authoritative G6 result.

### 5. Obtain independent exact-candidate evidence

Provide the frozen profile, snapshot, generated bundle, two Explorer receipts,
question suite, rights review, risk register and change-impact report to a
reviewer independent of the implementation. The review must explicitly cover:

- Stage 1 scope, authority, exclusions and source identities;
- all 24 question judgements, forbidden targets and required caveats;
- rights, privacy, credentials, unsafe URLs and access boundaries;
- accessibility, keyboard journeys, security and performance limits; and
- semantic assertions, evidence binding and asserted-versus-inferred claims.

Then run the formal evaluator with the independent acceptance review and the
exact search receipt. Do not substitute the deterministic diagnostic for this
step:

```bash
set -euo pipefail
.venv/bin/python -B scripts/evaluate.py \
  --bundle bundle \
  --output validation/candidate-v0.3.0/evidence/evaluation-acceptance.json \
  --k 10 \
  --min-expected-source-success-at-k 1.0 \
  --min-expected-target-recall-at-k 0.90 \
  --min-all-expected-target-success-at-k 1.0 \
  --min-mrr 0.80 \
  --acceptance-review validation/candidate-v0.3.0/evidence/evaluation-acceptance-review.json \
  --runtime-journeys evaluation/explorer-search-calibration-v0.3.0.json \
  --runtime-receipt validation/candidate-v0.3.0/evidence/explorer-search-v0.3.0-candidate.json
```

The independent review file is an input supplied for the exact frozen
candidate; the evaluator does not create that review. A pass is valid only if
the evaluator confirms its question-suite and release-root bindings and the
runtime receipt confirms the pinned Explorer and exact v0.3.0 search manifest.
Require exit status zero and an output with schema
`okf-hmlr-evaluation-acceptance.v1`, `status: pass` and
`g5_acceptance: pass`. The search receipt is the formal G5 runtime input; the
product receipt remains G4/G6 evidence.

### 6. Create reproducible G8 candidate packaging

After G1–G7 evidence is stable for the same frozen identity, build the
unreleased candidate archive twice to distinct paths and retain both receipts:

```bash
set -euo pipefail
.venv/bin/python -B -m unittest \
  tests.test_package_release \
  tests.test_release_metadata -v

.venv/bin/python -B scripts/package_release.py \
  --bundle bundle \
  --config source/build-config.json \
  --candidate-commit-sha <candidate-commit-sha> \
  --output dist/okf-landregistry-0.3.0-candidate-a.zip \
  --receipt validation/candidate-v0.3.0/evidence/release-candidate-archive-a.json

.venv/bin/python -B scripts/package_release.py \
  --bundle bundle \
  --config source/build-config.json \
  --candidate-commit-sha <candidate-commit-sha> \
  --output dist/okf-landregistry-0.3.0-candidate-b.zip \
  --receipt validation/candidate-v0.3.0/evidence/release-candidate-archive-b.json

cmp -s \
  dist/okf-landregistry-0.3.0-candidate-a.zip \
  dist/okf-landregistry-0.3.0-candidate-b.zip
shasum -a 256 \
  dist/okf-landregistry-0.3.0-candidate-a.zip \
  dist/okf-landregistry-0.3.0-candidate-b.zip

runtime_cache="$(mktemp -d "${TMPDIR:-/tmp}/okf-python-cache.XXXXXX")"
chmod 700 "$runtime_cache"
.venv/bin/python -I -B -X "pycache_prefix=$runtime_cache" \
  scripts/create_release_metadata.py \
  --candidate-commit-sha <candidate-commit-sha> \
  --archive-receipt validation/candidate-v0.3.0/evidence/release-candidate-archive-a.json \
  --output-directory validation/candidate-v0.3.0/evidence/release-metadata
```

The metadata command accepts only the designated
`validation/candidate-v0.3.0/evidence/release-candidate-archive-a.json` input
and the exact
`validation/candidate-v0.3.0/evidence/release-metadata` output directory. It
rejects the reproducibility-only `-b` receipt, historical locations,
`final-g9/` and any other candidate descendant. The metadata writer holds each
repository-relative directory by a no-follow descriptor, stages the complete
two-file directory privately and publishes an absent target with one atomic
no-replace directory rename. It
preserves byte-identical existing files, including their inode and modification
time, and fails before publication if the exact existing two-file set differs. All
configuration, build receipt, workflow, dependency-lock, builder and
provenance-material inputs are bounded bytes read from the named immutable
candidate commit; later worktree replacements cannot enter the generated
documents. The designated archive receipt is opened securely once, and the
same bounded byte buffer is both parsed and validated.

The governed directory is therefore either absent or complete; it is never
published as a one-file intermediate. A racing writer is accepted only when it
has already published the exact complete byte-identical directory. Cleanup
failure can leave only a hidden `.release-metadata-*` stage, which candidate and
evidence inventory checks reject. The command also revalidates the held parent
chain and fails closed on platforms without the required no-follow,
directory-descriptor and atomic no-replace rename semantics.

Both ZIP files must be byte-identical and both receipts must report the same
archive SHA-256 and bundle release root. Designate the `-a` receipt as the G8
metadata input and retain the `-b` receipt as the independent reproducibility
comparison. The receipt schema and publication state must remain
`okf-hmlr-candidate-archive.v1` and `unreleased-candidate`. These are
reviewable pre-G9 candidate artefacts, not an RC or released archive; do not
edit or relabel them. Confirm that `source/build-config.json` keeps
`release_at` null, record both commands' printed archive SHA-256 values, and
rehash the generated SBOM and provenance files. Each archive receipt must also
contain the exact governed candidate identity; a configuration or candidate
commit that differs from the build receipt must fail before packaging.
Packaging streams regular bundle members in bounded chunks through held,
no-follow descriptors; it does not materialise a large member in memory. The
temporary deterministic ZIP must be no more than 50,000,000 bytes before it
can replace the requested output, matching the later G8 evidence ceiling.

Before accepting the provenance as G8 evidence, confirm its observed Python
runtime exactly matches the runtime recorded by the candidate build receipt,
its invocation
names the exact frozen snapshot directory above and its materials include the
digest of `contracts/okf-explorer.consumer-lock.json`. A receipt whose declared
environment, invocation or Explorer lock differs from the build is diagnostic
only and cannot pass G8. Final G8 validation requires the exact version-scoped
archive receipt, SBOM and provenance paths, rehashes each secure input buffer,
then independently derives the canonical SBOM and provenance bytes from the
candidate commit and archive receipt. Self-consistent but invented metadata
therefore cannot pass merely by updating its evidence digest.

### 7. Assemble reviewable evidence, then stop

Only after G1–G8 source evidence exists for one identity may the pre-G9
assembler create a review index:

```bash
set -euo pipefail
.venv/bin/python -B scripts/assemble_release_evidence.py \
  --pre-g9 \
  --input validation/candidate-v0.3.0/assembly-inputs/pre-g9.json \
  --candidate-commit-sha <candidate-commit-sha> \
  --output-directory validation/candidate-v0.3.0/pre-g9

git add -- \
  validation/candidate-v0.3.0 \
  dist/okf-landregistry-0.3.0-candidate-a.zip \
  dist/okf-landregistry-0.3.0-candidate-b.zip
.venv/bin/python -B scripts/check_release_transition.py staged-evidence
git commit -m "Record v0.3.0 G1-G8 candidate evidence"
.venv/bin/python -B scripts/check_release_evidence.py \
  --candidate-only \
  --candidate-commit-sha "$CANDIDATE_SHA"
```

This command does not run checks, manufacture independence or authorise
publication. The evidence commit preserves the exact archive, receipts, source
evidence, assembly input and pre-G9 manifest that the reviewer will inspect.
The staged-evidence command must exit zero. It reads Git path output as
NUL-delimited bytes and permits only new regular stage-zero blobs on the exact
v0.3 evidence/archive surface; a failure means the commit must not be created.
Review the emitted receipt hashes and residual risks before asking the
independent release reviewer to assess that exact committed pre-G9 manifest.

Record the independent result in a separate JSON evidence artefact before
asking the owner for Land Registry G9. It has this exact shape; replace every
placeholder with the reviewed values rather than copying this example as
evidence:

```json
{
  "$schema": "https://chris-page-gov.github.io/okf-LandRegistry/schemas/release-evidence.schema.json",
  "schema": "okf-independent-release-review-evidence.v1",
  "candidate": {
    "candidate_commit_sha": "<40 lowercase hex>",
    "release_root_sha256": "<64 lowercase hex>",
    "checksums_sha256": "<64 lowercase hex>",
    "profile_pack_root_sha256": "<64 lowercase hex>",
    "snapshot_manifest_sha256": "<64 lowercase hex>"
  },
  "independent_review": {
    "identity": "<reviewer>",
    "kind": "ai-agent",
    "role": "release-reviewer",
    "reviewed_at": "<UTC timestamp ending Z>",
    "independent": true,
    "outcome": "recommend_approval"
  },
  "pre_g9_manifest_sha256": "<64 lowercase hex>",
  "approved_claims": ["<exact reviewed public claim>"],
  "residual_risk_ids": ["<exact reviewed governed risk ID>"]
}
```

The review artefact must use the same complete candidate, exact review object,
pre-G9 digest, ordered claim list and complete residual-risk set later written
to G9. Its SHA-256 is part of the owner's binding. A review statement embedded
only in the final release record is not independent digest-bound evidence.

After the owner has made the exact decision, commit the independent review and
completed final assembly input as a second evidence-only descendant before G9
assembly. Do not amend the earlier evidence commit:

```bash
set -euo pipefail
git add -- \
  validation/candidate-v0.3.0/evidence/independent-release-review.json \
  validation/candidate-v0.3.0/assembly-inputs/final-g9.json
.venv/bin/python -B scripts/check_release_transition.py staged-evidence
git commit -m "Record v0.3.0 independent review and owner decision"
.venv/bin/python -B scripts/check_release_evidence.py \
  --candidate-only \
  --candidate-commit-sha "$CANDIDATE_SHA"
```

The eventual final input must use `okf-release-assembly-input.v2`. Do not add
a generic `approved: true` object to the pre-G9 gate input. Record the owner's
decision with an `owner_approval.binding` that includes:

- the exact version and canonical URL derived from the digest-bound
  `source/build-config.json` (`version` and `publication_base`); the build
  receipt must inventory that source file exactly once with its current
  SHA-256 and repeat the same version and publication base; the builder must
  reject any CLI publication base that differs from the governed value;
- the complete candidate identity, including the exact commit and release
  root;
- the reviewed pre-G9 manifest path and SHA-256;
- the exact SHA-256 for every G1–G8 receipt in that manifest;
- the complete list of public claims the owner approves; and
- `governance/risk-register.json`, its exact SHA-256 and its complete risk-ID
  set;
- the exact disclosure of any incomplete human audit;
- the exact independent-review object from the review artefact; and
- the review artefact's repository-relative path and SHA-256.

Use strict RFC 3339 UTC timestamps ending in `Z`. For each reviewed check,
`completed_at` must be no later than the matching reviewer's `reviewed_at`,
which must be no later than the gate's `executed_at`. Every G1–G8 gate must be
no later than the pre-G9 manifest's `generated_at`; that time must be no later
than `independent_review.reviewed_at`; review must be no later than
`owner_approval.approved_at`; and approval must be no later than the final
evidence manifest's `generated_at`. Equal timestamps are allowed. If any event
is rerun or edited, create a new pre-G9 manifest and review rather than changing
a timestamp to make the order appear valid.

Use stable, trimmed, non-blank and control-free identity and role strings. The
project owner must not be declared as an independent gate or release reviewer,
including by adding whitespace to make the same identity look different. The
same identity cannot change kind or independence between gates.

The automated assembler accepts only `human_audit.status: not_completed` and
requires its risk ID to remain in the complete governed residual-risk set. A
claimed completed human audit requires a future, separately reviewed and
digest-bound human-audit workflow; it must not be represented by adding a
reviewer object to this input.

The final assembler reads each bounded input, schema and evidence file through
a secure repository-relative descriptor walk. It derives both a referenced
file's digest and parsed JSON from the same bytes, rejects symbolic links and
non-regular files, and fails closed where secure no-follow directory opening is
unavailable. It rehashes the bound materials and requires its newly generated
G1–G8 bytes to equal the owner-approved receipt hashes. Any difference requires
a new pre-G9 review and owner decision; the assembler must never infer or
extend the scope of approval.

The identity strings are exact attestations, not authenticated identities or
digital signatures. `RISK-018` must remain in the complete owner-reviewed risk
set. Use the controlled owner/reviewer procedure and do not describe the
resulting evidence as signed or cryptographically authenticated.

After the independent review and exact owner decision have been recorded in
the version-scoped final input, assemble G9 only into the v0.3.0 directory:

```bash
set -euo pipefail
.venv/bin/python -B scripts/assemble_release_evidence.py \
  --input validation/candidate-v0.3.0/assembly-inputs/final-g9.json \
  --candidate-commit-sha <candidate-commit-sha> \
  --output-directory validation/candidate-v0.3.0/final-g9

git add -- validation/candidate-v0.3.0/final-g9
.venv/bin/python -B scripts/check_release_transition.py staged-evidence
git commit -m "Record v0.3.0 final G9 evidence"
EVIDENCE_SHA="$(git rev-parse HEAD)"
.venv/bin/python -B scripts/check_release_evidence.py \
  --candidate-only \
  --candidate-commit-sha "$CANDIDATE_SHA"
.venv/bin/python -B scripts/check_release_evidence.py \
  --manifest validation/candidate-v0.3.0/final-g9/release-evidence.json \
  --candidate-commit-sha "$CANDIDATE_SHA"
```

Do not use `--replace` for a first assembly or to change reviewed evidence.
An exact same-byte rerun is permitted; any changed input must use a new output
directory and receive a new independent review and owner decision. The
root-level `validation/release-evidence.json`, `validation/release-record.json`
and `validation/receipts/` remain immutable v0.2.0 history.

The final `scripts/check_release_evidence.py` run repeats these comparisons
from the written evidence. For version `0.3.0`, the schema and checker both
reject a G9 record without the complete binding, so hand-authoring a generic
owner object cannot bypass the assembler. Historical unbound v0.1.0 and v0.2.0
evidence remains readable only when the complete candidate identity and both
the frozen G9-record SHA-256 and frozen evidence-manifest SHA-256 match the
recorded exception, calculated from the actual parsed G9 and manifest byte
buffers rather than caller-supplied digest strings. Relabelling a new candidate,
or changing either historical file, never bypasses the binding and is not
accepted as v0.3.0 approval.
The frozen `G9 / manifest` SHA-256 pairs are:

- v0.1.0: `6a50ff8e542c59d7d270aff20a6ce0582e58e8d66a350361cc456bcd1474657d` /
  `746e5a840fbacb195d738d5be17246da1f2969cce2743b1d3acc072ba8d13b62`;
  and
- v0.2.0: `46aadf4563f878285de3155124870be7144ec91f9f00e825c0e297b5618e2c11` /
  `facafcc21bf0b69a8b97a47df0cfd334b0a45f30680c6dc69c9c623f5423be9f`.

## Release-time actions outside this runbook

The G8 ZIP is an unreleased-candidate archive, not an RC or final release. No
RC tag, GitHub prerelease, Pages deployment, approval variable, final tag or
public-URL verification is permitted until an explicit exact-digest Land
Registry G9 owner decision has been assembled and this passes:

```bash
set -euo pipefail
CANDIDATE_SHA="${CANDIDATE_SHA:?set the immutable candidate commit SHA}"
.venv/bin/python -B scripts/check_release_evidence.py \
  --manifest validation/candidate-v0.3.0/final-g9/release-evidence.json \
  --candidate-commit-sha "$CANDIDATE_SHA"
```

After G9, first integrate the exact linear candidate-and-evidence history by
non-force fast-forward only. `EVIDENCE_SHA` must still name the checked commit
that contains the final G9 evidence. Push that descendant to the existing
candidate branch first, create or update its pull request, and require the PR
workflow to validate both the first immutable candidate commit and the final
manifest against the same `$CANDIDATE_SHA`. The release actor must set
`OKF_REMOTE`, `OKF_PR_NUMBER`, `CANDIDATE_SHA` and `EVIDENCE_SHA`.
`candidate/v0.3.0` is a governed release coordinate, not an overridable branch:

```bash
set -euo pipefail
umask 077
OKF_REMOTE="${OKF_REMOTE:?set the verified canonical GitHub remote name}"
OKF_REPOSITORY="chris-page-gov/okf-LandRegistry"
OKF_CANDIDATE_BRANCH="candidate/v0.3.0"
OKF_DEFAULT_BRANCH="main"
OKF_PR_NUMBER="${OKF_PR_NUMBER:?set the v0.3.0 pull-request number}"
CANDIDATE_SHA="${CANDIDATE_SHA:?set the immutable candidate commit SHA}"
EVIDENCE_SHA="${EVIDENCE_SHA:?set the final evidence commit SHA}"

.venv/bin/python -B scripts/check_release_transition.py \
  remote-binding --remote "$OKF_REMOTE"
git check-ref-format "refs/heads/$OKF_CANDIDATE_BRANCH"
git check-ref-format "refs/heads/$OKF_DEFAULT_BRANCH"
test "$(git rev-parse --verify HEAD)" = "$EVIDENCE_SHA"
git merge-base --is-ancestor "$CANDIDATE_SHA" "$EVIDENCE_SHA"
.venv/bin/python -B scripts/check_release_evidence.py \
  --candidate-only \
  --candidate-commit-sha "$CANDIDATE_SHA"
.venv/bin/python -B scripts/check_release_evidence.py \
  --manifest validation/candidate-v0.3.0/final-g9/release-evidence.json \
  --candidate-commit-sha "$CANDIDATE_SHA"

git fetch "$OKF_REMOTE" "$OKF_DEFAULT_BRANCH" "$OKF_CANDIDATE_BRANCH"
git merge-base --is-ancestor \
  "refs/remotes/$OKF_REMOTE/$OKF_DEFAULT_BRANCH" "$CANDIDATE_SHA"
git push "$OKF_REMOTE" \
  "$EVIDENCE_SHA:refs/heads/$OKF_CANDIDATE_BRANCH"

gh pr view "$OKF_PR_NUMBER" -R "$OKF_REPOSITORY" \
  --json state,isCrossRepository,headRepository,baseRefName,headRefName,headRefOid,reviewDecision | \
  .venv/bin/python -B scripts/check_release_transition.py pr-state \
    --expected-head-oid "$EVIDENCE_SHA" \
    --require-review-decision APPROVED
gh pr checks "$OKF_PR_NUMBER" -R "$OKF_REPOSITORY" \
  --watch --required --fail-fast
CHECK_STATE_DIR="$(mktemp -d \
  "${TMPDIR:-/tmp}/okf-required-checks.XXXXXX")"
chmod 700 "$CHECK_STATE_DIR"
REQUIRED_CHECKS_JSON="$CHECK_STATE_DIR/required-checks.json"
WORKFLOW_JSON="$CHECK_STATE_DIR/workflow.json"
WORKFLOW_RUN_JSON="$CHECK_STATE_DIR/workflow-run.json"
WORKFLOW_JOBS_JSON="$CHECK_STATE_DIR/workflow-jobs.json"
gh pr checks "$OKF_PR_NUMBER" -R "$OKF_REPOSITORY" \
  --required --json name,workflow,event,state,bucket,link \
  > "$REQUIRED_CHECKS_JSON"
CHECK_RUN_ID="$(.venv/bin/python -B scripts/check_release_transition.py \
  required-check-run-id --required-checks "$REQUIRED_CHECKS_JSON")"
gh api --hostname github.com \
  "repos/$OKF_REPOSITORY/actions/workflows/pages.yml" \
  > "$WORKFLOW_JSON"
gh api --hostname github.com \
  "repos/$OKF_REPOSITORY/actions/runs/$CHECK_RUN_ID" \
  > "$WORKFLOW_RUN_JSON"
gh api --hostname github.com \
  "repos/$OKF_REPOSITORY/actions/runs/$CHECK_RUN_ID/jobs?per_page=100" \
  > "$WORKFLOW_JOBS_JSON"
.venv/bin/python -B scripts/check_release_transition.py required-checks \
  --required-checks "$REQUIRED_CHECKS_JSON" \
  --workflow "$WORKFLOW_JSON" \
  --workflow-run "$WORKFLOW_RUN_JSON" \
  --workflow-jobs "$WORKFLOW_JOBS_JSON" \
  --expected-head-oid "$EVIDENCE_SHA"
gh pr view "$OKF_PR_NUMBER" -R "$OKF_REPOSITORY" \
  --json state,isCrossRepository,headRepository,baseRefName,headRefName,headRefOid,reviewDecision | \
  .venv/bin/python -B scripts/check_release_transition.py pr-state \
    --expected-head-oid "$EVIDENCE_SHA" \
    --require-review-decision APPROVED

git fetch "$OKF_REMOTE" "$OKF_DEFAULT_BRANCH" "$OKF_CANDIDATE_BRANCH"
test "$(git rev-parse --verify \
  "refs/remotes/$OKF_REMOTE/$OKF_CANDIDATE_BRANCH")" = "$EVIDENCE_SHA"
git merge-base --is-ancestor \
  "refs/remotes/$OKF_REMOTE/$OKF_DEFAULT_BRANCH" "$CANDIDATE_SHA"
gh ruleset check --default -R "$OKF_REPOSITORY"
test "${OKF_DIRECT_PUSH_AUTHORISED:-}" = "yes"

git push --dry-run "$OKF_REMOTE" \
  "$EVIDENCE_SHA:refs/heads/$OKF_DEFAULT_BRANCH"
git push "$OKF_REMOTE" "$EVIDENCE_SHA:refs/heads/$OKF_DEFAULT_BRANCH"
git fetch "$OKF_REMOTE" "$OKF_DEFAULT_BRANCH"
test "$(git rev-parse --verify \
  "refs/remotes/$OKF_REMOTE/$OKF_DEFAULT_BRANCH")" = "$EVIDENCE_SHA"

RELEASE_ROOT="$(.venv/bin/python -B scripts/check_release_transition.py \
  bundle-inventory --bundle bundle)"
OKF_RELEASE_ROOT_SHA256="$RELEASE_ROOT" \
  .venv/bin/python -B scripts/check_release_approval.py
gh variable set OKF_RELEASE_ROOT_SHA256 \
  --body "$RELEASE_ROOT" -R "$OKF_REPOSITORY"
test "$(gh variable get OKF_RELEASE_ROOT_SHA256 \
  --json value --jq .value -R "$OKF_REPOSITORY")" = "$RELEASE_ROOT"

PREVIOUS_RUN_ID="$(gh run list -R "$OKF_REPOSITORY" \
  --workflow pages.yml --event workflow_dispatch \
  --branch "$OKF_DEFAULT_BRANCH" --commit "$EVIDENCE_SHA" --limit 1 \
  --json databaseId --jq '.[0].databaseId // empty')"
gh workflow run pages.yml -R "$OKF_REPOSITORY" \
  --ref "$OKF_DEFAULT_BRANCH" \
  -f expected_commit_sha="$EVIDENCE_SHA" \
  -f expected_release_root_sha256="$RELEASE_ROOT"
DEPLOYMENT_RUN_ID=""
for _attempt in {1..15}; do
  DEPLOYMENT_RUN_ID="$(gh run list -R "$OKF_REPOSITORY" \
    --workflow pages.yml --event workflow_dispatch \
    --branch "$OKF_DEFAULT_BRANCH" --commit "$EVIDENCE_SHA" --limit 1 \
    --json databaseId --jq '.[0].databaseId // empty')"
  if [[ -n "$DEPLOYMENT_RUN_ID" && "$DEPLOYMENT_RUN_ID" != "$PREVIOUS_RUN_ID" ]]; then
    break
  fi
  sleep 2
done
test -n "$DEPLOYMENT_RUN_ID"
test "$DEPLOYMENT_RUN_ID" != "$PREVIOUS_RUN_ID"
gh run watch "$DEPLOYMENT_RUN_ID" -R "$OKF_REPOSITORY" --exit-status
```

The PR must be open, originate in the canonical repository, target `main`, use the non-overridable
`candidate/v0.3.0`, have the exact `EVIDENCE_SHA` as its head, be approved and
have every required check passing. The bounded JSON check resolves the check
link through the authoritative GitHub workflow, workflow-run and job APIs. It
requires the canonical repository, `.github/workflows/pages.yml`, the
`pull_request` event, exact `EVIDENCE_SHA`, and one successful `verify` job;
the same display name from another workflow, run, commit or repository cannot
substitute.
The state binding is repeated after the checks to close a head-movement race.
Both pushes are intentionally non-force. The final push must be a fast-forward;
if remote `main` moves, stop rather than merging or rebasing after the candidate
freeze. Create a new candidate, rerun affected gates and obtain a new G9
decision.

`gh ruleset check` is a mandatory preflight, but it cannot grant bypass rights.
Before setting `OKF_DIRECT_PUSH_AUTHORISED=yes`, the authorised release actor
must confirm that repository branch protection permits this exact direct,
non-force fast-forward (or that the actor has an explicitly governed bypass).
A standard merge commit, squash, rebase or merge-queue result changes the
approved identity and is not an alternative. If direct integration is not
permitted, stop and change the governed release procedure before attempting
promotion.

Default-branch push workflows verify but never upload or deploy. Only the
explicit dispatch after the remote branch equals `EVIDENCE_SHA` may deploy. It
receives the exact commit and root as required inputs and proves that
`github.sha`, the commit input, an immediately re-fetched canonical remote
`main`, the root input, the repository variable and the ignored-inclusive
bundle inventory all agree before upload. Create the GitHub
prerelease from the same evidence commit and G8 archive, then perform
the bounded 60-second real-browser identity and journey check. Promote only if
it passes, reusing the same archive and bundle bytes without rebuilding or
repackaging. A verification failure means no promotion. Do not edit
`CITATION.cff`, the changelog, documentation or any other governed source at
promotion; any such change creates a new candidate, reopens the affected gates
and requires a new G9 decision.

If an explicit deployment dispatch is not observed or fails without changing
reviewed bytes, do not rebuild, edit or create a new commit. Re-verify the exact
remote commit, local bytes and variable, then dispatch the same immutable
identity again and watch the newly created run:

```bash
set -euo pipefail
OKF_REMOTE="${OKF_REMOTE:?set the verified canonical GitHub remote name}"
OKF_REPOSITORY="chris-page-gov/okf-LandRegistry"
OKF_DEFAULT_BRANCH="main"
EVIDENCE_SHA="${EVIDENCE_SHA:?set the final evidence commit SHA}"
RELEASE_ROOT="${RELEASE_ROOT:?set the exact approved release root}"

.venv/bin/python -B scripts/check_release_transition.py \
  remote-binding --remote "$OKF_REMOTE"
git fetch "$OKF_REMOTE" "$OKF_DEFAULT_BRANCH"
test "$(git rev-parse --verify HEAD)" = "$EVIDENCE_SHA"
test "$(git rev-parse --verify \
  "refs/remotes/$OKF_REMOTE/$OKF_DEFAULT_BRANCH")" = "$EVIDENCE_SHA"
.venv/bin/python -B scripts/check_release_transition.py \
  bundle-inventory --bundle bundle --expected-root "$RELEASE_ROOT"
test "$(gh variable get OKF_RELEASE_ROOT_SHA256 \
  --json value --jq .value -R "$OKF_REPOSITORY")" = "$RELEASE_ROOT"

PREVIOUS_RUN_ID="$(gh run list -R "$OKF_REPOSITORY" \
  --workflow pages.yml --event workflow_dispatch \
  --branch "$OKF_DEFAULT_BRANCH" --commit "$EVIDENCE_SHA" --limit 1 \
  --json databaseId --jq '.[0].databaseId // empty')"
gh workflow run pages.yml -R "$OKF_REPOSITORY" \
  --ref "$OKF_DEFAULT_BRANCH" \
  -f expected_commit_sha="$EVIDENCE_SHA" \
  -f expected_release_root_sha256="$RELEASE_ROOT"
RECOVERY_RUN_ID=""
for _attempt in {1..15}; do
  RECOVERY_RUN_ID="$(gh run list -R "$OKF_REPOSITORY" \
    --workflow pages.yml --event workflow_dispatch \
    --branch "$OKF_DEFAULT_BRANCH" --commit "$EVIDENCE_SHA" --limit 1 \
    --json databaseId --jq '.[0].databaseId // empty')"
  if [[ -n "$RECOVERY_RUN_ID" && "$RECOVERY_RUN_ID" != "$PREVIOUS_RUN_ID" ]]; then
    break
  fi
  sleep 2
done
test -n "$RECOVERY_RUN_ID"
test "$RECOVERY_RUN_ID" != "$PREVIOUS_RUN_ID"
gh run watch "$RECOVERY_RUN_ID" -R "$OKF_REPOSITORY" --exit-status
```
