# Recovery compatibility M17 will use this matrix for upgrades; M15 establishes it and proves the fail-closed directions. ## Backup schema versus ModelForge build | Backup Alembic revision | Restoring build | Result | | --- | --- | --- | | equals the build's head | same or newer build | restored as is | | older, present in the build's chain | newer build | restored, then migrated forward to head | | absent from the build's chain | older build | `SCHEMA_TOO_NEW` — refused | | recorded as none | any build | preflight fails; the backup is not restore eligible | | any | build shipping no Alembic chain | `SCHEMA_UNKNOWN` — refused | The chain is walked by `down_revision`, not by filename order, so a revision that sorts earlier but descends later is still placed correctly. Discovery resolves the source checkout, the installed image and an explicitly configured `MODELFORGE_ALEMBIC_DIRECTORY`. A newer backup is never silently downgraded onto older code. Restoring an older backup onto a newer build runs the forward migrations that build ships and nothing else. ## PostgreSQL | Source major | Destination major | Result | | --- | --- | --- | | 17 | 17 | supported | | 17 | 18+ | permitted; preflight records both majors | | 17 | 16 or lower | `POSTGRES_VERSION_INCOMPATIBLE` | The control-plane image pins its client tools to the server major version. `pg_dump` from an older major cannot read a newer server, which is why the tools ship with the control plane rather than being inherited from the host. ## Manifest schema `manifest_schema_version` is `m15.1`. Verification requires the manifest file and the journaled `backup_manifest_entries` to agree exactly, so a manifest written by a different schema version is rejected rather than partially interpreted. ## Semantic fingerprint `FINGERPRINT_VERSION` is `m15.2`; `m15.2` adds the schema-0024 `audit_chain_heads` singleton to the audit group. Current schema-0024 fingerprints are comparable only when both sides are `m15.2` and both contain exactly one complete checkpoint row. Both fingerprints must enumerate the complete authoritative table contract: a missing expected table, an unexpected table, an absent/sparse entry or a malformed count/digest is incompatible. After reconciliation, every non-current-truth table must retain its exact count and digest except the enumerated mutations: one canonical audit marker, the corresponding singleton-head advance, and same-count suppression changes in `operational_alerts`. Every fingerprint also records the exact physical table-name set, including current-truth tables whose rows are intentionally excluded. Dropping `host_telemetry_latest`, adding an attacker table, or omitting any authoritative table is therefore incompatible. Table digests and group/overall digests use typed, length-prefixed cells and rows; separators, nulls and different scalar types cannot alias. A table exceeding the 250,000-row cap is marked `BOUNDED`; that is useful diagnostic evidence but can never authorize READY because it is not a complete count-and-digest proof. There is one explicit cross-version rule: a backup whose recorded Alembic revision is exactly `20260828_0022` and whose fingerprint is `m15.1` may be compared with a restored/migrated `m15.2` fingerprint when the old side has no checkpoint and the new side has exactly one. Strict audit-chain verification must pass independently. The transition still permits exactly one canonical audit marker and no other digest/count change (apart from same-count alert suppression). No other absent or introduced table, fingerprint-version pair or schema revision receives that allowance. Auth migration `20260830_0023` adds constraints only, so it introduces no physical-table exception inside this schema-0022-to-0024 comparison. ## Encryption `MFBK1` frames the encrypted payload with the key id it was sealed under. A payload sealed by a different key id is still authenticated by GCM and fails closed. The algorithm is recorded per backup set, so introducing a second algorithm later does not invalidate existing backups. ## Recovery contract Recovery API responses never carry a DSN with its password, a credential hash, or an enrolment token. `RestorePlanResponse` exposes `database_destination_redacted` and has no `database_destination` field at all, which is asserted against the published OpenAPI document.