Order the records the way the system is learned
Jochen asked whether the order made sense. It did not -- it followed when things happened to be decided, which after consolidation is fictional anyway since record 5 alone folds decisions taken across a week. Concretely wrong before: the domain statement sat at 8, after five engineering rules; the constitution was scattered across 5, 12 and 17; the tiers landed at 15, 16, 21 and 22 with process records in between. Now it walks: what the mesh is (1-3), its tiers from the bottom up (4-8), what runs on them and how it gets there (9-10), how it is built (11-16), how it is checked (17-18), how we work (19-23). Two things made this safe rather than free. It is a permutation, not a compaction, so the renames go through temporary names -- otherwise two files want one slot and one is lost. And the reference rewrite is a single simultaneous pass, because almost every number moved into a slot another number was vacating; replacing one at a time would have cascaded and pointed things at the wrong record while still resolving. Verified: 284 [ADR NNNN](path) links across the repository, all with matching text and target. The ordering principle is now stated in 19 rather than left implicit -- the repository already said "the numbering is the flow" about its folders, and there was no reason for the records to be the exception.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
---
|
||||
status: accepted
|
||||
date: 2026-05-14
|
||||
deciders: jochen
|
||||
reconstructed: true
|
||||
---
|
||||
|
||||
# 13. Schema and state changes are numbered migrations, in the same language as the code
|
||||
|
||||
> Reconstructed after the fact from the evidence cited below.
|
||||
|
||||
## Context
|
||||
|
||||
Modules own persistent state. That state has to change as they change, across nodes that are
|
||||
at different versions, some of which have data that predates the change.
|
||||
|
||||
Two things were being done that do not survive contact with a second node. Schema was created
|
||||
at startup, so what a table looked like depended on which version last started. And migrations
|
||||
were shell scripts, so they could not use the types, connection handling or helpers the module
|
||||
already had, and were not compiled or checked with it.
|
||||
|
||||
## Considered options
|
||||
|
||||
1. **Startup SQL / create-if-missing.** Rejected. It converges only for a node that started
|
||||
with the newest version. A node that never restarts never migrates; a node that restarts on
|
||||
an old version can undo a change.
|
||||
2. **Shell migrations.** Rejected. Unchecked, untyped, and a separate dialect from the module
|
||||
they belong to. Also, as later discovered, packaged differently — and therefore
|
||||
occasionally not packaged at all.
|
||||
3. **Numbered migrations in the module's own language, compiled with it.** Chosen.
|
||||
|
||||
## Decision
|
||||
|
||||
Every schema or state change is a numbered migration file, written in the same language as the
|
||||
module and compiled with it. There is no startup schema creation and no ad-hoc statement.
|
||||
|
||||
Rules that come with it:
|
||||
|
||||
- The initial migration is **frozen** once it has run anywhere. It is never modified; a change
|
||||
is a new number.
|
||||
- Every statement is **idempotent** — guarded so that re-running is safe.
|
||||
- A change needs **both** a baseline for a fresh installation and an incremental migration for
|
||||
installations that already exist. Code referencing a column requires that the migration
|
||||
creating it exists.
|
||||
- Migration numbers are unique. A duplicate prefix is a defect, not a style issue.
|
||||
|
||||
## Consequences
|
||||
|
||||
- A node at any version converges to the current schema by running the migrations it has not
|
||||
run.
|
||||
- Migrations are checked by the same compiler as the code, and a migration that does not
|
||||
compile fails the build rather than the deployment.
|
||||
- The rules are enforced unevenly. Duplicate prefixes have shipped repeatedly and been fixed by
|
||||
renumbering afterwards; the mesh now validates for them, which is the check this rule needed
|
||||
in order to be real.
|
||||
- A migration directory is a feature like any other, which means it is packaged like any other
|
||||
— and when packaging is wrong, migrations silently do not ship. This has happened.
|
||||
- Freezing the initial migration means a fresh installation replays the entire history. That
|
||||
cost grows and nothing currently bounds it.
|
||||
|
||||
## References
|
||||
|
||||
- `fix(agents): convert workflow migrations to TypeScript` (#50), 2026-05-14.
|
||||
- `feat(dev_validate): guard against duplicate migration numeric prefixes` (#379), 2026-06-26 —
|
||||
the rule acquiring a check.
|
||||
- Knowledge base: `migrations/schema-drift`, `noxflow/troubleshooting/migration-number-collision`.
|
||||
- Packaging failures: `troubleshooting/shell-migrations-never-packaged`,
|
||||
`troubleshooting/provision-migration-never-applied`.
|
||||
Reference in New Issue
Block a user