Smeldr has never had a schema migration mechanism. Every table-creating function uses CREATE TABLE IF NOT EXISTS, which is exactly right for a fresh instance and does precisely nothing for one that already exists in an older shape. Until now, that gap was theoretical, a risk named for when M6 starts handing real instances to pilots. It stopped being theoretical the moment we went looking for the mechanism to build.
The fix already existed, four times
Before designing anything, we grepped every table-creating function in the package. Four of them already solve exactly this problem, by hand, independently: read PRAGMA table_info, check whether a column is present, ALTER TABLE … ADD COLUMN if it isn't. One of them, the function that added Rev for optimistic concurrency, has been shipping since v1.42.1. Nobody had named the pattern; four different fixes had each quietly reinvented it.
That's the answer to the question we expected to spend the most time arguing: does this need an ordered migration-file list with a version table, the way most frameworks solve this? No, that would be new machinery, imported wholesale, for a problem this codebase has already been solving correctly by hand. The fix is EnsureColumn, one function that generalizes what already worked, and four call sites that shrink to one line each.
The bug that was waiting to be found
While confirming the motivating incident, CreateSiteConfigTable shipping without the columns Node requires, hand-patched twice by two different downstream repos, we didn't take the history at its word. We read the current DDL. scheduled_at and rev were still missing.
So we wrote a one-off test: create the table with today's code, save a SiteConfig through it. It failed immediately: no such column: scheduled_at. Not a hypothetical about instances upgrading from an old release. A brand-new instance, provisioned today, could not save a SiteConfig at all. The two existing tests for this table had never actually called Save, which is exactly how something this broken ships unnoticed for months.
The fix mirrors a bug this project already fixed once before, in a different table: declare the columns in the CREATE TABLE text itself *and* keep a migration path for whoever already has the broken version. Belt and suspenders isn't overcaution here: it's the only way to close both the fresh-install case and the upgrade case with one change.
What this doesn't try to solve
EnsureColumn has no opinion about who is responsible for calling it. A column belongs to whoever declared the Go field for it: the framework calls it for framework-owned columns, and an application extending a framework table (adding a custom field to SiteConfig, say) calls it itself, the same way it already calls CreateSiteConfigTable. There's no registry, no discovery, no "the framework will notice your struct changed." That's a deliberate boundary, not a missing feature: a mechanism that guessed at a caller's own schema requirements would be guessing, and a wrong guess against a live table is worse than no mechanism at all.