Contents

Migration Model Fleet — What a Release Must Rehearse

A history entry that applies cleanly to the model's validation surface proves nothing about the live databases behind it, because that surface is current and a current database has nothing pending. Before a release carries an entry, a throwaway copy of every live database the model owns is converged through that model's real boot sequence. Each model in a project's migration_model capability declares what that means for it in the project's migration_fleet capability, {"models": {"<model>": <fleet>}}. The fleet is release-evidence configuration, so it is its own document beside the authoring declaration. There is one preflight and one release gate for every project; the declaration is the only per-project part.

Declaration

kind Databases rehearsed Boot sequence Other keys
engine_tenants The engine's tenant databases on the environment's admin cluster, rostered with the reason each candidate is or is not a member The engine's boot-time schema and history convergence none
named_databases names: databases on the environment's admin cluster converge_argv, run from the project checkout with the copy's DSN bound to runner.config.connection_env_var; optional verify_argv runs the same way afterwards and must exit 0 schema_shape_sources: checkout-relative files whose content is the schema the boot converges
none Nothing Nothing reason: why this model has no live databases a release can rehearse
// The engine's own model, under "models": {"primary": ...}.
{"kind": "engine_tenants"}

// A service converging its own registry database at boot.
{
  "kind": "named_databases",
  "names": ["service_registry"],
  "converge_argv": ["uv", "run", "--project", "services/svc", "python", "-m", "svc.schema", "init"],
  "verify_argv": ["uv", "run", "--project", "services/svc", "python", "-m", "svc.schema", "status"],
  "schema_shape_sources": ["services/svc/src/svc/schema.py"]
}

// One database on a host the admin cluster cannot copy.
{"kind": "none", "reason": "single SQLite database on the production host"}

A release gate refuses a model that declares no fleet, and a fleet for a model the project's migration_model does not declare: an undeclared fleet and an empty one are different facts. Create the document with yoke projects capability-settings set --project P --cap-type migration_fleet --new --settings-json '{"models": {...}}'; change one model with capability-settings merge --set models.<model>=<fleet JSON>. Writing the capability takes project-admin authority (projects.capability_settings.set / merge), because a named_databases fleet names commands the release machinery runs on an operator's machine. Those commands receive a minimal environment — locale, home, temp, certificate paths, the project's UV_* settings, the copy's DSN, and its restore point — never the operator's credentials, and their failure output has the copy's DSN and password removed.

The admin cluster is the environment's release.admin_connection setting on the project's own environment. A named_databases fleet runs from the project's checkout registered on this machine (yoke project register), so the declared commands find the code they converge with. A non-Python schema source is digested byte for byte; a Python one by its syntax tree, so comments and docstrings do not invalidate coverage.

Fleet copy admission is automatic at the shared rehearsal kernel: an exclusive OS file lock identifies the physical local cluster and disposable database name. It refuses immediately with rehearsal_copy_busy, holder diagnostics and a retry after the holder finishes; the refused runner neither copies nor cleans up. Admission covers dump, restore, the entire pending-history batch, invariants or diagnostic callbacks, connection close and cleanup. Transfer and declared-command children retain admission if their parent dies, until the children stop. Stable lock files are retained; metadata is diagnostic and never decides ownership. Distinct clusters and copy names proceed independently. This protects callers of the kernel, including yoke watch preflight, not manual/ad hoc database operations. The persistent yoke migration rehearse ITEM validation database and its existing migration-territory claim remain reusable and unchanged.

Preflight

yoke watch preflight -- --project P [--model M] [--checkout PATH] \
    <environment> [db ...] \
    --record-receipt --product-sha SHA --receipt-env <control-plane>

The preflight runs from the installed Yoke package in any working directory; no Yoke source checkout or PYTHONPATH is needed. A declared project boot command still uses that project’s checkout. Startup and subprocess failures name a reason and a diagnostic capture; read it, fix the named cause and retry. For a missing packaged module, reinstall with yoke update or report a defective release and its capture to the control-plane operator.

--model defaults to the capability's default_model. --checkout names the checkout a named_databases fleet converges from (default: the project's checkout registered on this machine). The live databases are only read: each is dumped, restored into the local embedded cluster with parallel pg_restore jobs, converged, verified, and dropped. A passing run with --record-receipt writes the covered history entry names and the schema-shape digest onto that project environment's own settings document under release.fleet_rehearsal.<model>, on the prod release-gate control plane. Coverage is the union of passing runs and never ages out; one environment's receipt never satisfies another, and one model's never covers a sibling model that shares an entry name. A receipt claims the whole declared fleet, so naming databases is a diagnostic run and --record-receipt refuses it.

For a read-only census of the restored source, reuse that same copy path:

yoke watch preflight -- --project P [--model M] <environment> [db ...] --instant-census-output DIR --copy-budget-gib N --minimum-free-gib N

This diagnostic mode does not apply history, converge schema or exercise release-driver writes. It inspects a read-only copy transaction, retains aggregate instant counts and archive/database sizes with phase timings before cleanup, and cannot record a release receipt. Counts include blanks, missing columns, unclassified candidate columns, invalid calendars and unknown offsets. Separate format-shape counts identify qualified calendar text (including PostgreSQL offset spellings), calendars without offsets, dates without times, and other values. These shapes do not certify calendar validity or timezone provenance; collection success does not authorize conversion. Original row bodies are not logged. The explicit disk budget also counts concurrent filesystem consumption, and the free-space floor remains required throughout each guarded transfer. Put retained output on the copy filesystem. Inspect all refusals before retrying or authoring a repair; this mode supplies no timezone or nullability decisions.

Release gate

After real boot convergence and history invariants on each engine tenant copy, product sessions, frontier, board, launch get/list, message-attempt and item get/list readers must consume the copied rows. Per-record session/item reads and paged launch reads cover rows beyond list windows. A refused or crashing reader fails preflight with its operation and recovery; it earns no fleet receipt. Once the readers pass, the verifier runs the secret-receipt replay proof directly on the same converged copy and transaction: it inherits no rehearsal module context or skip, a copy that still holds one-time secret receipts refuses before replay, and the replayed scrub must rewrite nothing and leave the ledger and event receipts byte-identical. A replay failure fails preflight with its named secret_receipt_replay_failed reason. Only then do the release-surface checks run. The first failure wins and is reported with its own operation and recovery. All these operations stay inside the existing copy admission and cleanup. Named-database fleets retain their project-declared boot and verification commands.

Before a deployment run dispatches a GitHub workflow, it rehearses every project that workflow ships: the run's own project at its release commit, and each project a stage binds through input_bindings at the commit the run recorded for that binding (a Yoke release that carries a Platform pin ships Platform's registry history this way). Each project, model and target environment is checked once per attempt; a bound project is checked under its environment of the same name, from its checkout registered on this machine, fetching the bound commit when that checkout lacks it. A binding with no recorded commit, two commits of one project, or a bound project with no registered checkout or no readable environment refuses the dispatch with its recovery. When preparation re-binds a stale source just before dispatch, the new commit is checked again before the workflow goes out.

For each shipped project the gate reads every model's fleet. A none fleet is skipped with its reason printed. An undeclared fleet refuses the dispatch with the declaration recipe. Otherwise the gate reads that commit's history entries (from the model's runner.config.modules_dir) and schema-shape digest, and when the target environment's receipts do not cover both it rehearses exactly that commit's code before dispatching. The installed Yoke package only orchestrates the copy; the candidate commit supplies the history, schema and boot it is judged by. The rehearsal runs while the stage's CI gate waits on the same commit, so it adds no wait of its own once CI is the longer one; dispatch still needs both the passing gate and the covering receipt, and a failed CI gate is the refusal reported. A named_databases fleet converges from a disposable checkout of that commit, and an engine_tenants fleet rehearses only after the dispatching engine's history bytes and schema shape are proven identical to the commit's (otherwise it refuses and names the release-driver recovery). A Yoke self-release driver, stage and production alike, executes the release commit itself, so that proof holds without any local install of the release. It then re-reads coverage and refuses if the receipt still falls short. The Yoke release workflow's pre-tag check, require_fleet_migration_preflight --project P <environment>, reads the same declarations and model-scoped receipts from a checkout of the release commit.

Item rehearsal is a different receipt

yoke migration rehearse PREFIX-N proves the item's entry against the validation surface and writes its receipt to migration_audit on the control plane that holds the item, where the implementing -> reviewing-implementation gate reads it. Fleet coverage is release evidence and lives on environment settings. Neither substitutes for the other.

Migration fleets · Yoke