Observation from #11535 / #11720 (detection of a multi-value field left on a stale varchar/text column). Filed rather than fixed — no user-visible defect today, but the shape forced a design compromise and the next author will hit it again.
What was measured
ManagedDriftEntry.category has three values, and every consumer routes on it:
| category | dev auto-reconcile | os migrate apply (no flag) | artifact-pinned boot gate |
|---|
safe | applies | applies | applies, boot continues |
needs_confirm | skips | applies | applies, boot continues |
destructive | skips | skips | refuses the boot |
Measured against the real consumers: runArtifactBootMigrationGate (packages/cli/src/utils/artifact-boot-migration.ts:137) filters category === 'destructive' and returns ok=false, which the caller turns into a thrown boot failure on kernel:ready, before the HTTP socket opens. A needs_confirm entry returns ok=true.
There is no fourth state meaning "report this, never act on it". A divergence that only a human may resolve therefore has two bad options and no good one:
destructive — reports loudly, but refuses the boot of every deployment that has the divergence. For a finding whose whole audience is already-serving databases, that turns the report into the outage.needs_confirm — does not refuse the boot, but is handed to applyMigrationEntries, which has no arm for the op and declines it.
#11720 took the second and relies on the decline. It works, and it is pinned, but it works by omission rather than by declaration: nothing states that the op is report-only except a doc comment, and a future author adding a reconciler arm "for completeness" would silently change what os migrate apply does to a corrupt production column.
Second, smaller half: the decline message is wrong
When the reconciler declines an op it has no arm for, it says (sql-driver.ts, end of applyDriftOpInPlace):
[schema-drift] manual_column_type_change on t.col is unsupported on dialect 'postgres' — skipped
It is not a dialect limitation — the op is unsupported on every dialect, deliberately. An operator reading that line reasonably concludes their dialect is the problem and that another one would migrate the column for them.
Sketch, not a proposal
Either a DriftCategory member (report_only) that every consumer must handle, or a REPORT_ONLY_DRIFT_OPS set alongside the existing INDEX_DRIFT_OPS, read by applyMigrationEntries so such ops are partitioned out before the dialect switch and reported with their own wording. The second is smaller and needs no consumer to re-derive a boundary — the property artifact-boot-migration.ts explicitly warns against ("two opinions is how one of them ends up wrong").
Both touch sql-driver.ts and packages/cli, i.e. outside #11720's declared surface, which is why this is a note and not a diff.
Observation from #11535 / #11720 (detection of a multi-value field left on a stale
varchar/textcolumn). Filed rather than fixed — no user-visible defect today, but the shape forced a design compromise and the next author will hit it again.What was measured
ManagedDriftEntry.categoryhas three values, and every consumer routes on it:os migrate apply(no flag)safeneeds_confirmdestructiveMeasured against the real consumers:
runArtifactBootMigrationGate(packages/cli/src/utils/artifact-boot-migration.ts:137) filterscategory === 'destructive'and returnsok=false, which the caller turns into a thrown boot failure onkernel:ready, before the HTTP socket opens. Aneeds_confirmentry returnsok=true.There is no fourth state meaning "report this, never act on it". A divergence that only a human may resolve therefore has two bad options and no good one:
destructive— reports loudly, but refuses the boot of every deployment that has the divergence. For a finding whose whole audience is already-serving databases, that turns the report into the outage.needs_confirm— does not refuse the boot, but is handed toapplyMigrationEntries, which has no arm for the op and declines it.#11720 took the second and relies on the decline. It works, and it is pinned, but it works by omission rather than by declaration: nothing states that the op is report-only except a doc comment, and a future author adding a reconciler arm "for completeness" would silently change what
os migrate applydoes to a corrupt production column.Second, smaller half: the decline message is wrong
When the reconciler declines an op it has no arm for, it says (
sql-driver.ts, end ofapplyDriftOpInPlace):It is not a dialect limitation — the op is unsupported on every dialect, deliberately. An operator reading that line reasonably concludes their dialect is the problem and that another one would migrate the column for them.
Sketch, not a proposal
Either a
DriftCategorymember (report_only) that every consumer must handle, or aREPORT_ONLY_DRIFT_OPSset alongside the existingINDEX_DRIFT_OPS, read byapplyMigrationEntriesso such ops are partitioned out before the dialect switch and reported with their own wording. The second is smaller and needs no consumer to re-derive a boundary — the propertyartifact-boot-migration.tsexplicitly warns against ("two opinions is how one of them ends up wrong").Both touch
sql-driver.tsandpackages/cli, i.e. outside #11720's declared surface, which is why this is a note and not a diff.