zanith

migrate / risk · 01 — The model

Six levels. Twenty reasons. Three gates.

Every op Zanith can emit has a published level. Every level has a numeric score on a 0–100 scale. Every score is gateable in CI. The data below is verbatim from engine/src/migrations/risk.ts — the same classifier the runner uses.

the ladder
  • 5
    safe
  • 15
    low
  • 35
    medium
  • 60
    high
  • 80
    destructive
  • 95
    blocked

--max-risk N refuses to apply anything above N. Defaults to (off).

every op kind, classified
createTablecreateExtensionaddColumn (nullable)addColumn (default)alterColumnDefaultalterColumnNullable (→ nullable)addIndexdropIndexaddUniquedropUniqueaddForeignKeydropForeignKeyrenameTablerenameColumnaddGeneratedColumnenableRlsdisableRlscreatePolicydropPolicysoftDropColumnsoftDropTablearchiveColumnarchiveTablebackfillsplitColumnmergeColumnsrawSqladdColumn (NOT NULL, no default)alterColumnTypealterColumnNullable (→ NOT NULL)rebuildTabledropExtensiondropColumndropTable

Color tracks level; ops cluster by danger. Notice that addColumn appears in three places — its level depends on whether it's nullable, has a default, or is required without one.

34 op kinds · 6 levels · 21 reason codespure classifierno DB callsrisk.ts · v0.2

02 — Ops, by family

Drops cluster red. Adds cluster green.

Eight families, each with its own danger profile. Color tracks level — emerald for safe, amber for medium, rose for destructive. Spot the gradient at a glance.

Additions

adding things rarely breaks production

addColumn (nullable)addColumn (default)addColumn (NOT NULL)addIndexaddUniqueaddForeignKeyaddGeneratedColumncreateTablecreateExtension

Drops

removing data is expensive — most of these need consent

dropColumndropTabledropExtensiondropUniquedropForeignKeydropIndex

Alters

type and nullability changes can fail silently — flagged early

alterColumnDefaultalterColumnNullable (→ nullable)alterColumnNullable (→ NOT NULL)alterColumnType

Renames

renaming breaks any client reading the old name — prefer expand-contract

renameTablerenameColumn

RLS / policy

RLS changes affect every read and write on the table

enableRlsdisableRlscreatePolicydropPolicy

Recovery-aware (Phase 2)

the safe alternatives — restorable until cleanup

softDropColumnsoftDropTablearchiveColumnarchiveTable

Structural

long-running. Use during off-peak windows.

backfillsplitColumnmergeColumnsrebuildTable

Escape hatch

the planner can't reason about it — review by hand

rawSql

03 — Reasons

Why an op got that level — in two words.

Every operation carries one or more reason codes alongside its risk level. Pipelines that need to gate on the cause — not just the severity — read these. Twenty-one codes, grouped into seven families.

Additions

6
  • adds_nullable_columnback-compat
  • adds_required_column_without_defaultfails on data
  • adds_indexslow on big tables
  • adds_unique_constraintfails on dupes
  • adds_foreign_keyfails on orphans
  • adds_extensionidempotent

Drops

6
  • drops_column_with_datadata lost
  • drops_table_with_datadata lost
  • drops_indexplan regression
  • drops_unique_constraintdupes possible
  • drops_foreign_keyorphans possible
  • drops_extensioncascading break

Alters

3
  • narrows_column_typetruncation
  • changes_column_typedata lost or fails
  • changes_column_nullable_to_not_nullfails on NULLs

Structural

2
  • rebuilds_tablefull row copy
  • requires_backfilllong running

Policy

1
  • rls_changeevery read affected

Renames

2
  • rename_tablebreaks old clients
  • rename_columnbreaks old clients

Escape hatch

1
  • raw_sqlunclassifiable

04 — The gauntlet

Three gates. All must pass.

An op moves from parsed to appliedthrough three checkpoints. They don't replace each other — they layer. Tighten any one of them in CI without touching the others.

oppreflightdata-shape probe01--max-riskscore budget02--allow-destructiveexplicit consent03applypreflight finding → exit 1score > N → exit 1no flag → exit 1

preflight failed

A column has NULLs but the op wants NOT NULL. The migration never starts.

score exceeds budget

An op scores 80 but CI's --max-risk is 35. The plan is rejected before any write.

destructive without consent

A drop is in the plan but --allow-destructive wasn't passed. Fix the op or pass the flag — never silent.

all gates pass

Preflight is clean, score is under budget, destructive ops have consent. Apply runs and audits each step.