migrate / risk
01 — The classifier
Six levels. Twenty-one reasons. Seven gates.
Every op Zanith emits carries a published level, score, and reason code from risk.ts. CI wires --max-risk against the numbers — not opinions. blocked is reserved for future row-count-aware refusals; the classifier does not assign it today.
31 kinds. 7 families.
types.ts. Each op maps to a level + score in risk.ts — no hand-maintained marketing table.Create
4 opsadding things rarely breaks production
createExtensioncreateTableaddColumnaddGeneratedColumnIndex & constraint
6 opsconstraints can fail on real data
addIndexdropIndexaddUniquedropUniqueaddForeignKeydropForeignKeyAlter
5 opstype and nullability changes need preflight
alterColumnTypealterColumnNullablealterColumnDefaultrenameTablerenameColumnDrop
3 opsremoving data is expensive — most need consent
dropColumndropTabledropExtensionSecurity (RLS)
4 opsRLS changes affect every read and write
enableRlsdisableRlscreatePolicydropPolicyRecovery & advanced
8 opsrestorable until cleanup or purge
softDropColumnsoftDropTablearchiveColumnarchiveTablebackfillsplitColumnmergeColumnsrebuildTableEscape hatch
1 opsthe planner can't reason about it — review by hand
rawSql21 codes, four families.
Additions
adds_nullable_columnadds_required_column_without_defaultadds_indexadds_unique_constraintadds_foreign_keyadds_extension
Drops
drops_column_with_datadrops_table_with_datadrops_indexdrops_unique_constraintdrops_foreign_keydrops_extension
Alters
narrows_column_typechanges_column_typechanges_column_nullable_to_not_nullrebuilds_tablerequires_backfill
Structural & raw
rls_changeraw_sqlrename_tablerename_column
7 gates. All must pass.
--max-risk <n>Aborts if any op's score exceeds the ceiling. Production caps at 60, staging at 80.
--allow-destructiveDROP COLUMN / DROP TABLE refuse without it. No implicit consent for data loss.
Four data probes run before apply: UNIQUE dupes, FK orphans, NULLs before NOT NULL, populated tables before NOT NULL adds.
pg_advisory_xact_lock around the apply loop — two deploys can't race the same database.
Each migration's ops run in one transaction. A failed step rolls back and is recorded as failed.
--shadow-verifiedIn production mode, up refuses unless verify ran on a shadow DB (override with --skip-shadow-check).
--dry-runPrints the plan, risks, and preflight results. Touches nothing in production.
Set migrationSafety.mode and the gates inherit a posture. The default when unset is staging — safe by default.
| mode | max-risk | destructive | advisory lock | preflight | shadow |
|---|---|---|---|---|---|
| dev | 100 | allowed | off | off | off |
| staging | 80 | needs flag | on | on | recommended |
| production | 60 | blocked | on | on | required |
Risk is the lens. See it in motion.
The classifier feeds every other stage. Here's where the score actually does its work.
Lifecycle, in depth
Generate, plan, verify, apply, audit — every flag, every output, every artifact path.
/migrate/lifecycleShadow-DB verify
The 4 internal stages of verifyOnShadow, deployment topologies, 4 verdicts.
/migrate/verifyRecovery
5 artifact kinds, the bookkeeping table, the 7-verb recover CLI.
/migrate/recoverAudit + history
5 Postgres tables track every migration, step, snapshot, and artifact.
/migrate/auditelsewhere