Skip to content

feat(core,platform-objects,spec): ADR-0119 D2 — the migration-journal runner (#4617) - #4668

Merged
os-zhuang merged 1 commit into
mainfrom
claude/focused-mendel-5akrna
Aug 2, 2026
Merged

feat(core,platform-objects,spec): ADR-0119 D2 — the migration-journal runner (#4617)#4668
os-zhuang merged 1 commit into
mainfrom
claude/focused-mendel-5akrna

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Implements ADR-0119 D2 (#4617). Closes the gap D1 left open.

Acceptance, as the ADR states it: a migration killed mid-run is either resumable to completion or compensable to clean, with journal rows proving which.

Why a journal, given D1 already gave us transactions

ADR-0119 D1 made engine.transaction() contract-reachable, which is the right answer for multi-write atomicity that fits in one transaction. Migration-class work does not fit:

  • a million-row backfill cannot hold one write-lock for its duration;
  • driver-memory's beginTransaction deep-clones the entire database — O(db) per begin;
  • ObjectQL.transaction() binds the default driver only, so a multi-datasource migration silently commits part of its work outside it;
  • a process killed — as distinct from a thrown error — defeats in-process rollback entirely, and that is the case operators actually hit.

So the unit of atomicity is the chunk, and durability across chunks is the journal.

Four consumers had each converged on the same four moves — preflight, undo journal, LIFO compensation, re-entrant recovery (ADR-0105 D13 promotion, ADR-0117 D8's ownership backfill, the org lifecycle transitions, D10 master-data distribution #4585). One copy is engineering; four is platform debt.

The invariant that carries the design

chunk_done(i) is written inside the chunk's own transaction, so done ⇔ committed holds by construction. chunk_started(i) is written autonomously, before it.

That asymmetry looks untidy and is load-bearing: it gives started ∧ ¬done exactly one meaning — the outcome is unknown — which is the only state a crash can leave and the only state recovery reasons about. Symmetrising it would destroy recovery. It is called out in the object doc, the runner header, and the test that pins which writes carry a transaction context.

What's here

runMigrationJournal (@objectstack/core) — preflight dry-run across every step before any step writes; chunked writes each inside engine.transaction(); LIFO compensation newest-first on failure, each in its own transaction; re-entrant forward recovery under a per-plan onCrash: 'resume' | 'compensate'; at-least-once delivery with an attempt counter, reusing bulk-write.ts's contract verbatim rather than deriving a second delivery-semantics story.

sys_migration_journal (@objectstack/platform-objects) — rows keyed (run_id, seq) under a unique index, so a resumed run that miscomputes its next sequence fails loudly instead of double-recording. Registered unconditionally beside sys_migration, because recovery must be discoverable with zero host wiring (ADR-0078: a journal nobody re-reads is a journal that does not exist). Distinct in grain from sys_migration — that holds one durable verdict per named migration; this holds many rows per run. Read-only over the API; writes go through the runner in system context.

Row contract in @objectstack/spec/system so core's runner can write the journal without depending on platform-objects.

It refuses rather than degrades

Four places: the runtime cannot roll back; any preflight fails; the plan declares onCrash: 'compensate' but a step cannot compensate; or a resume's plan hash disagrees with the journal — resuming a changed plan would apply chunk boundaries the journal never described, so "chunk 7 done" would name different rows and the resume would skip work it never did.

A compensation failure halts and is journalled — never swallowed — and the run ends failed, not compensated. A database in a state no clean story covers must not be reported as a tidy rollback.

engineCanRollBack is now shared

The two-level probe (engine method and default-driver beginTransaction) was the same condition written twice — here and in batchData's atomic gate. It moves to @objectstack/core as a type predicate and @objectstack/metadata-protocol imports it. Two copies of "can this runtime actually roll back?" drift by one clause and leave one caller believing it has atomicity it does not have. The D4 refusal semantics are unchanged — its 10 tests pass untouched in substance.

ADR renumber

ADR-0118 (plugin-reachable transactions) is renumbered ADR-0119. It merged 2026-08-02 12:11, one day-part after an unrelated ADR-0118 (非用户 actor 的平台契约) at 10:37; the earlier merge holds the number. All 30-odd code citations, both anchors entries and the pending changeset move with it; the non-user-actor record is untouched.

Its Status line also stops saying "implemented in this PR" — a dangling reference once merged — and now cites the implementing PR plus the key file and test for D1 and D4, and records explicitly that D2/D3 are not implemented.

Scope

Deliberately not here, per the split agreed for #4617: boot reconciliation and os migrate resume. They are the discovery channel and depend on this core landing first. findInterruptedRuns — the primitive they consume — is exported and tested here. #4617 stays open until that second PR lands.

Verification

  • packages/core/src/utils/migration-journal.test.ts19 new tests, against a fake engine implementing real rollback, so a failed transaction discards its writes including the chunk_done row inside it. Without that, every assertion would also pass against a runner that never opened a transaction — the exact bug class D4 existed to kill. Covers all three ADR acceptance cases (crash mid-chunk → resume exactly once with idempotency honoured; crash mid-compensation → halts loudly, no silent partial; plan-hash mismatch → refuses), plus LIFO order, the capability gate, preflight fail-closed, discovery, and sequence continuity across a resume.
  • Full suites green: core 456, spec 7285, platform-objects 254, metadata-protocol 216, objectql D4 e2e 6.
  • pnpm typecheck — 122/122. pnpm lint clean. check:adr-anchors OK. check:generated — 8/8 artifacts current (regenerated api-surface/docs for the new spec exports).

packages/spec/src/system/constants/platform-object-names.ts needed the new object added — the repo's declared-vs-registered conformance test caught it, which is the mechanism working.

Refs: ADR-0119 D2, #4617, #4612, ADR-0034, ADR-0060, ADR-0078, ADR-0117 D8


Generated by Claude Code

… runner (#4617)

A migration killed mid-run is now either resumable to completion or
compensable to clean, with journal rows proving which.

ADR-0119 D1 made `engine.transaction()` contract-reachable, which answers
multi-write atomicity that fits in one transaction. Migration-class work does
not fit: a million-row backfill cannot hold one write-lock, driver-memory's
`beginTransaction` deep-clones the whole database, `transaction()` binds the
default driver only, and a KILLED process defeats in-process rollback
entirely. So the unit of atomicity is the chunk, and durability across chunks
is a journal.

- `runMigrationJournal` (@objectstack/core): preflight dry-run across every
  step before any step writes; chunked writes each inside `engine.transaction()`;
  LIFO compensation newest-first on failure; re-entrant forward recovery under
  a per-plan `onCrash` policy; at-least-once with an `attempt` counter, reusing
  bulk-write.ts's delivery contract rather than re-deriving it.
- `sys_migration_journal` (@objectstack/platform-objects): rows keyed
  (run_id, seq) under a unique index, registered unconditionally beside
  sys_migration so recovery is discoverable with zero host wiring (ADR-0078).
  Distinct in grain from sys_migration, which holds one verdict per named
  migration; this holds many rows per run.
- Row contract + object-name constant in @objectstack/spec/system, so core's
  runner writes the journal without depending on platform-objects.

The invariant carrying the design: `chunk_done(i)` is written INSIDE the
chunk's transaction so `done ⇔ committed` holds by construction, while
`chunk_started(i)` is written autonomously before it. That asymmetry gives
`started ∧ ¬done` exactly one meaning — outcome unknown — which is the only
state a crash leaves and the only state recovery reasons about.

The runner refuses rather than degrades: no rollback capability, a failed
preflight, an uncompensable plan declaring onCrash:'compensate', or a resume
whose plan hash disagrees with the journal. A compensation failure halts and
is journalled, and the run ends `failed` rather than `compensated` — a
database in a state no clean story covers must not be reported as a tidy
rollback.

`engineCanRollBack` is now shared: the two-level probe was the same condition
in this runner and in batchData's atomic gate, and two copies drift by one
clause and leave one caller believing it has atomicity it does not have. It
moves to @objectstack/core as a type predicate; metadata-protocol imports it.

Boot reconciliation and `os migrate resume` land separately; the discovery
primitive they consume, `findInterruptedRuns`, is exported here.

Docs: ADR-0118 (plugin-reachable transactions) is renumbered ADR-0119. It
merged a day after an unrelated ADR-0118 (非用户 actor 的平台契约), and the
earlier merge holds the number. Its Status line now cites the implementing
PR and its tests instead of a dangling "this PR", and records that D2/D3
remain unimplemented.

Refs: ADR-0119 D2, #4617, #4612, ADR-0034, ADR-0060, ADR-0078, ADR-0117 D8

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NKcGqCYCCpMkB5UW8jNPXx
@vercel

vercel Bot commented Aug 2, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 2, 2026 3:37pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation protocol:system tests tooling size/xl labels Aug 2, 2026
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/core, @objectstack/metadata-protocol, @objectstack/platform-objects, @objectstack/spec.

115 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/actions-as-tools.mdx (via @objectstack/core)
  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/knowledge-rag.mdx (via @objectstack/core)
  • content/docs/ai/natural-language-queries.mdx (via @objectstack/core)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata-protocol, packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/core, packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/core)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/core)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/core, @objectstack/metadata-protocol, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/core)
  • content/docs/permissions/authorization.mdx (via packages/core, @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/anatomy.mdx (via @objectstack/core)
  • content/docs/plugins/development.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/core, @objectstack/platform-objects, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/core, @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v15.mdx (via @objectstack/core)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/platform-objects, @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants