Skip to content

C11: adopt core's migration ledger on witnesses, not trust (epic byte5ai/omadia#470) - #6

Merged
Weegy merged 1 commit into
mainfrom
feat/c11-adopt-core-ledger
Aug 20, 2026
Merged

C11: adopt core's migration ledger on witnesses, not trust (epic byte5ai/omadia#470)#6
Weegy merged 1 commit into
mainfrom
feat/c11-adopt-core-ledger

Conversation

@Weegy

@Weegy Weegy commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Epic byte5ai/omadia#470 C11, plugin side. Core half: byte5ai/omadia#806 (adds ctx.sql.seedLedger, plugin-api 1.3.0).

The problem

Slots 00220030 are already applied on every installation that ran the Dev Platform inside core — recorded in core's ledger (_multi_orchestrator_migrations). This plugin's ledger (plg_omadia_dev_platform_migrations) starts empty, so runMigrations() re-applies all nine on the first activation after the upgrade. They are idempotent, so on a healthy database that is merely slow. But idempotence is a property of the files, and betting an upgrade on it nine times over is not a plan.

Why the obvious handoff is wrong

Copy core's rows into this plugin's ledger and skip those files. Correct on a healthy database; silently destroys one specific installation:

rows present, tables ABSENT — a database restored from a snapshot taken before those migrations, a version-skewed rollback, an operator who dropped a table during an incident.

Nine rows get written, nothing is applied, the plugin activates green, and every request against those tables 500s.

So core's ledger is corroboration and a witness is the decision: a query against the live catalog, true only when the schema object that file creates is actually there.

What ships

  • src/ledgerHandoff.ts — the nine witnesses. Each proves the last object its file creates, because each core migration ran inside a single transaction: the last object exists exactly when the whole file was applied. One rule, checkable by a reviewer against the .sql in migrations/.

    File Proves
    0022_dev_platform.js table dev_job_artifacts
    0023_dev_platform_pipeline.js table dev_github_app_installations
    0024_dev_platform_w3.js column dev_jobs.conductor_await_id
    0025_dev_jobs_source_plugin.js constraint dev_jobs_source_check admitting 'plugin'
    0026_dev_job_gate_kind.js column dev_job_gates.gate_kind
    0027_dev_platform_triggers.js column dev_jobs.usage_estimated
    0028_dev_jobs_webhook_one_active.js index dev_jobs_webhook_one_active
    0029_dev_platform_retention.js index dev_jobs_terminal_ended_idx
    0030_dev_job_events_truncated_marker.js index dev_job_events_truncated_once_idx

    0025 is the odd one out: it replaces a CHECK constraint to admit a new source value, so its witness reads the constraint's pg_get_constraintdef for 'plugin'. The constraint name is present before the migration too, so presence alone would be true on a database that never ran it.

  • activate() seeds before it migrates, guarded on ctx.sql.seedLedger existing. A core older than plugin-api 1.3.0 falls through to the apply loop and says so in the log — declaring the method required would make this plugin refuse to activate on a core that can in fact run it.

  • A non-empty skippedNoWitness is a loud WARNING, not a refusal. The apply loop below is the repair, so refusing would replace a self-healing upgrade with an outage. But the operator has to be told: core's ledger and the live catalog disagree, and that means this database is not what they think it is.

  • handoff-plan.json, and it is REQUIRED in the ZIP. It is how an operator dry-runs the handoff against production before installing, with core's middleware/scripts/plugin-ledger-handoff.mjs --plan …. A ZIP without it installs perfectly and quietly removes the only step that de-risks the upgrade — the same reasoning that made migrations/ required after the first cut of build-zip.mjs shipped a ZIP without it.

Two traps, both encoded as tests

  • 'public.dev_jobs'::regclass throws for a missing table — the exact case a witness exists to detect — so a cast turns the restore scenario into an activation crash. Every witness uses to_regclass or a catalog join. A test rejects the cast syntax outright.
  • SELECT count(*) is not a witness: 1 for a table that exists, 0 for one that exists and is empty, a throw for one that does not. The kernel enforces exactly one row, one column, a real boolean; a test forbids count( here too.

Tests

12 new, all offline. What each one catches, since none of this can be proved against a database this repo can see:

Test Silent failure it prevents
covers exactly the shipped migrations a migration with no witness is re-applied on every upgrade — invisible, because the files are idempotent
in run order, each named once a duplicate makes the outcome depend on iteration order
never casts to regclass the restore scenario becomes an activation crash
single-row single-column boolean; no count(; no ; the kernel rejects the shape at runtime, in production
proves an object its own migration creates all nine touch dev_jobs, so naming a different migration's object is the easy mistake; this greps the migration's own SQL
0025 reads the constraint DEFINITION presence of the constraint alone is true before 0025 ran
handoff-plan.json matches the code the dry run an operator trusts would be describing a handoff the plugin no longer performs
activate() order is seed → migrate a seed after the apply loop is a no-op: the files are already in the ledger
activates without seedLedger and logs the degradation a hard requirement would break activation on an older core
warns, naming the files, on skippedNoWitness a silent disagreement is precisely what C11 exists to prevent

Mutation check: pointing 0029's witness at 0022's table (dev_job_artifacts) fails exactly one test — proves an object the migration it belongs to actually creates — and nothing else. 682 pass / 1 fail. The rule is enforced, not just documented.

Gates

Gate Result
npm run typecheck (all workspaces)
npm run build
npm test ✅ plugin 684/685 (1 pre-existing skip) · runner-shim 76 · daemon 454 · ui — 0 failures
npm run package ✅ ZIP builds, handoff-plan.json staged, 9 migrations verified, version drift guard passed

Ordering

This plugin is safe to merge before core #806: ctx.sql.seedLedger is guarded, so against today's core the handoff is skipped, the log says why, and the nine idempotent migrations are applied as they are today. The handoff starts working the moment a core carrying #806 is deployed.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

…0 C11)

Slots 0022-0030 are already applied on every installation that ran the Dev
Platform inside core, recorded in CORE's ledger. This plugin's ledger starts
empty, so `runMigrations()` re-applies all nine. They are idempotent, so that
is merely slow on a healthy database — but idempotence is a property of the
files, and betting an upgrade on it nine times over is not a plan.

`ctx.sql.seedLedger()` (core PR byte5ai/omadia#806, plugin-api 1.3.0) records
them as applied instead, and will not take core's word for it: each file needs
a WITNESS that the schema object it creates is actually present.

The case that makes this necessary is rows present, tables ABSENT — a restore
from a snapshot older than the migrations, a version-skewed rollback, an
operator who dropped a table during an incident. A handoff that trusted core's
rows would activate this plugin green and make every request 500. With
witnesses the seed declines, `runMigrations()` applies the files, and that is
the repair.

- `src/ledgerHandoff.ts` — the nine witnesses. Each proves the LAST object its
  file creates, because a core migration file ran in one transaction, so the
  last object exists exactly when the whole file was applied. 0025 is the odd
  one: it REPLACES a CHECK constraint, so its witness reads the constraint
  DEFINITION for 'plugin' — presence alone would be true before it ran too.
- `activate()` seeds BEFORE migrating, guarded on `ctx.sql.seedLedger` being
  present: a core older than plugin-api 1.3.0 falls through to the apply loop
  and says so, rather than refusing to activate on a core that can run it.
- A non-empty `skippedNoWitness` is logged as a loud WARNING, not a refusal —
  the apply loop below is the repair, and the operator needs to know the
  database is not what they thought.
- `handoff-plan.json` + REQUIRED in the ZIP. It is how an operator dry-runs the
  handoff against production BEFORE installing, with core's
  `middleware/scripts/plugin-ledger-handoff.mjs`. A ZIP without it installs
  perfectly and quietly removes the only step that de-risks the upgrade.

Tests: 12 new. The entry list must cover exactly the shipped migrations (a
missing witness re-applies forever, invisibly); no witness may cast to
regclass (it throws on the very case a witness detects); every witness must
name an object that appears in its own migration — all nine touch dev_jobs, so
proving the wrong one is the easy mistake; the plan file must match the code;
and activate() must seed before it migrates, degrade on an old core, and warn
on a disagreement. Mutation check: pointing 0029's witness at 0022's table
fails exactly the target test.
@Weegy
Weegy merged commit 7c56df0 into main Aug 20, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant