Contents

Database migration conventions

Use the timestamp, query-clock and migration-audit conventions when authoring or applying governed migrations.

Timestamp discipline

PostgreSQL instant columns use TIMESTAMPTZ; Python retains aware datetimes. Nullable instants use SQL NULL. The classified roster and serving catalog own storage declarations; permanent history owns governed conversion.

Application writers bind instant_parameter(conn, utc_now()) or a supplied parse_instant(value). Owned wire, JSON, file and SQLite values use canonical UTC YYYY-MM-DDTHH:MM:SS.ffffffZ and permitted nulls. Format with format_instant or the shared JSON encoder at those boundaries. Calendar dates and opaque strings keep their own contracts. Read domain instants for the shared kernel, strict input, historical conversion and wire recipes.

Query-Time SQL Clock Helpers

yoke_contracts.time_sql.now_sql(...) supplies native indexed predicates; fixed day/hour/minute offsets count elapsed seconds. Trusted offset_modifier SQL has caller-owned calendar meaning; board buckets declare their timezone. Python cutoffs bind through instant_parameter. instant_wire_sql(expression) projects canonical fixed-six UTC text after native filtering and ordering, preserving SQL NULL. See domain instants for query examples.

migration_model capability — recipe + runner vocabulary

Validation recipes and migration runners live behind the migration_model row, each model's release fleet behind the migration_fleet row: vocabulary, pairing matrix, and webapp Pack contract in model declaration and runner and audit, pairing and Webapp Packs; the fleet in migration fleets.

Live-apply provenance on migration_audit

A destructive apply refuses rather than writing nulls: session, actor, branch, and commit must be established first, and the receipt records them. Legacy null rows stay as incident evidence and are not backfilled. model_name is a declared migration model; an execution level in that column is refused.

The extra provenance columns below remain nullable so older rows stay readable. set_audit_provenance still skips unknown columns on a pre-migration authoritative DB.

actor_id          TEXT  -- harness_sessions.actor_id at apply time
worktree          TEXT  -- absolute worktree_path the apply ran from
source_branch    TEXT  -- git branch in that worktree (--show-current)
source_commit    TEXT  -- git HEAD sha in that worktree
integration_target TEXT -- typically 'main'
change_class      TEXT  -- profile.migration_strategy (additive_only, hard_cutover, expand_contract)

The migration_audit.lease_id join already linked the audit row to the coordination claim that protected it; the new columns answer "from which checkout, by which actor, against which integration target." Doctor's coordination-claims-unmerged-source HC uses source_branch + integration_target to flag completed rows whose source never reached integration target — a sign the worktree was deleted before the slice merged.

Applying a migration

There is no apply command. A server brings its own database up to the code it runs before it serves: converge_core_schema computes history - ledger, takes an exclusive per-database advisory lock, and applies each pending entry in order, committing the entry and its raw-byte SHA256 in the applied_migrations row in ONE transaction. A common non-NULL digest mismatch refuses before restore or mutation. Legacy NULL rows remain serviceable but are reported for explicit manifest-bound adoption; the mandatory prepare/verify/ apply procedure lives in migration-model.md. /v1/health reports migration_content_evidence_ready and refuses serving when the evidence schema or exact append-only guard behavior is not intact; container/deploy readiness also requires the adoption-required list to be empty. Boot is fail-hard, so a container never serves behind its schema or against known mismatched permanent migration bytes.

A work item authors the entry and rehearses it:

yoke --env <name> migration rehearse PREFIX-N

Rehearsal applies the entry to the validation surface, records the gate's receipt in migration_audit on the control plane that holds the item, and holds the LIVE_DB_MIGRATION:<model> coordination claim until the merge lands so a second item cannot enter migration territory meanwhile; failure releases it.

Breakage policy and migration strategy

projects.breakage_policy is founder_cutover (default) or compatibility_required. A DB claim declares migration_strategy (additive_only, hard_cutover, expand_contract); the gates combine them:

Policy Strategy Result
any additive_only allowed
founder_cutover hard_cutover allowed for pre_merge_safe or pre_merge_breaking
founder_cutover expand_contract needs migration_strategy_justification
compatibility_required hard_cutover needs migration_strategy_justification
Migration conventions · Yoke