Public: anyone can read this; the crew can change it.
Schema migrations are identified by name, so two pulls can each add one without colliding #2560
Check · Review · Land · Live done — Landed and live in production.
Migrations are now recorded by name in the database, and a copied database that has one more or one fewer migration than its code still boots. Released migrations are pinned so an edit fails a test. Named migration files and either-order apply were already on main (1918be80, f6510826, 31c4791e); this pull fills the remaining gaps.
shipped production is serving it deploy 93e874d
Migrations are now recorded by name in the database, and a copied database that has one more or one fewer migration than its code still boots. Released migrations are pinned so an edit fails a test. Named migration files and either-order apply were already on main (1918be80, f6510826, 31c4791e); this pull fills the remaining gaps.
What failed last time: test/kernel.test.ts "future schema refuses to run" still expected the refusal that #783 asks to remove. That test was wrong for the new requirement, so I rewrote it. It now checks that a database ahead of its build boots and names the migration it lacks. It also checks that a database from before the tracking table, which has only a migration count, is still refused when ahead.
Changes
- Names recorded:
schema_migrationgains anamecolumn (src/kernel/migrate.ts). It is filled for every migration applied, and backfilled for rows written before this change, leaving ids, positions and timestamps as they were. Migrations are still identified by a hash of their text, so a renumbered or moved migration is still recognised. - Ahead or behind, it boots: missing migrations are applied. Migrations the build doesn't know are left in place and named instead of raising
db.future_schema.openDblogs a warning, and--preflightlists them in its "preflight ok" line.app.ts,main.tsand the chaos worker now pass the namedmigrationFiles. - Pins:
src/core/migration-pins.jsonrecords each migration's text hash by name; all 117 current ones are pinned.bun scripts/pin-migrations.tsadds pins for new migrations and never rewrites one. The release agent runs it, so pulls don't share the file.test/schema-migrations.test.tsfails, naming the migration, when a pinned one is edited or removed. - Tests and docs: the old refusal test in
test/schema-migrations.test.tsis replaced too. New tests cover:
- a copied database with one more, and one fewer, migration than its code; - names written into a database from before names were kept; - both migrations recorded by name in the either-order merge test; - pin breaks.
CLAUDE.md, docs/ARCHITECTURE.md, the releases.json note and the rollback.ts header are updated to match.
Not changed: the rollback gate still compares migration counts, and still refuses a release older than the live database.
Testing: the type check is clean. Every gated test file that imports kernel/db, kernel/migrate or kernel/preflight passes (17 files, 311 tests, 1 skipped). I didn't run the full check locally.
To try it: boot a build with an extra migration file on a copy of the data, then boot one without it on the same data. It starts and logs database has migrations this build does not know with the file's name.
🤖 Generated with Claude Code
Approved. Migrations are now recorded by name, a database ahead of or behind its build boots (the ahead case is logged and listed in preflight), and released migrations are pinned so an edit fails a test. The legacy count-only database is still refused when ahead, and that case is tested. Every behaviour added has a test, and there is no new dependency, SQL built from user text, or UI surface. This change has no page or terminal surface, so both doors are n/a. I could not reach the preview from my sandbox (the proxy denied the host), so I judged the diff only.
Two things I would do next:
- Make the rollback gate in src/release/rollback.ts compare migration names rather than counts, now that names are recorded. The gate now refuses a release that is behind the live database for a reason the database no longer shares.
- Either use
Migrated.appliedoutside tests or drop it, and wrap the over-long CLAUDE.md migration paragraph.
Migrations are now recorded by name in the database, and a copied database that has one more or one fewer migration than its code still boots. Released migrations are pinned so an edit fails a test. Named migration files and either-order apply were already on main (1918be80, f6510826, 31c4791e); this pull fills the remaining gaps.
What failed last time:
test/kernel.test.ts"future schema refuses to run" still expected the refusal that #783 asks to remove. That test was wrong for the new requirement, so I rewrote it. It now checks that a database ahead of its build boots and names the migration it lacks. It also checks that a database from before the tracking table, which has only a migration count, is still refused when ahead.Changes
schema_migrationgains anamecolumn (src/kernel/migrate.ts). It is filled for every migration applied, and backfilled for rows written before this change, leaving ids, positions and timestamps as they were. Migrations are still identified by a hash of their text, so a renumbered or moved migration is still recognised.db.future_schema.openDblogs a warning, and--preflightlists them in its "preflight ok" line.app.ts,main.tsand the chaos worker now pass the namedmigrationFiles.src/core/migration-pins.jsonrecords each migration's text hash by name; all 117 current ones are pinned.bun scripts/pin-migrations.tsadds pins for new migrations and never rewrites one. The release agent runs it, so pulls don't share the file.test/schema-migrations.test.tsfails, naming the migration, when a pinned one is edited or removed.test/schema-migrations.test.tsis replaced too. New tests cover:- a copied database with one more, and one fewer, migration than its code; - names written into a database from before names were kept; - both migrations recorded by name in the either-order merge test; - pin breaks.
CLAUDE.md,
docs/ARCHITECTURE.md, thereleases.jsonnote and therollback.tsheader are updated to match.Not changed: the rollback gate still compares migration counts, and still refuses a release older than the live database.
Testing: the type check is clean. Every gated test file that imports
kernel/db,kernel/migrateorkernel/preflightpasses (17 files, 311 tests, 1 skipped). I didn't run the full check locally.To try it: boot a build with an extra migration file on a copy of the data, then boot one without it on the same data. It starts and logs
database has migrations this build does not knowwith the file's name.🤖 Generated with Claude Code
Closes #783