Skip to content

ADR-0067 Decision-2 unimplemented: package-draft publish is not turn-atomic — a commit CAN half-land #3066

Description

@os-zhuang

Summary

ADR-0067 Decision-2 promises "all writes of a commit land in ONE engine.transaction(); a commit cannot half-land." The 2026-07-16 status audit found this is not implemented: publishPackageDrafts promotes drafts per-item with catch-and-continue, so a mid-batch failure leaves earlier items live and later ones unpublished — the exact "Published! but half-landed" state the ADR exists to kill. For an AI-authoring platform this is a main-path risk: AI turns are batchy, and a partial landing is a metadata state nobody ever reviewed. Today's mitigation (revert the partial commit) is after-the-fact and requires someone to notice.

Verified current behavior (HEAD 2026-07-16)

  • Per-item catch-and-continue: metadata-protocol/src/protocol.ts:4711-4722 — a failing draft is pushed to failed[] and the loop continues; earlier publishMetaItem calls are already live.
  • Commit records the partial landing: recordPackageCommit runs after the loop over publishedKeys only (:4760-4774, insert at :5180).
  • Even a single promote is two transactions: repo.promoteDraft = put() (own withTxn) + best-effort delete() of the draft (second withTxn) — sys-metadata-repository.ts:577-636.
  • The test suite locks in the non-atomic contract: objectql/src/protocol-publish-package-drafts.test.ts:84-101 ("collects per-item failures without aborting the rest") asserts publishedCount === 2, failedCount === 1.
  • The namespace pre-check (protocol.ts:4621-4638) is the only all-or-nothing step today — the model to extend.

Design (two-phase publish)

Phase 1 — transactional metadata writes. Open ONE engine.transaction() (ADR-0034 ambient tx, objectql/src/engine.ts:2869-2896) around the whole batch: every draft's promoteDraft (put + draft-delete) and the sys_metadata_commit insert. Any failure → rollback → nothing landed, success:false, zero published.

Phase 2 — post-commit side effects (once, best-effort, in this order): in-memory registry mutations (applyObjectRegistryMutation), DDL (ensureObjectStorage/syncObjectSchema), batched applySeedBodies, publish materializers, mutation projectors (ADR-0094), emitMetadataMutation/watch events, build probes. These are exactly the effects that (a) cannot run inside a driver tx — DDL runs on bare knex and deadlocks SQLite's single-connection pool (sql-driver.ts:1429 assertBareKnexSafe); in-memory registry writes can't roll back — and (b) already have boot-reconciliation healing (the ADR-0094 lesson: side effects may self-heal, half-landed metadata cannot).

Blockers each requiring a real change

  1. Repo tx threading: promoteDraft/put/delete accept no ctx and withTxn (sys-metadata-repository.ts:238-243) always opens a FRESH engine.transaction — under an outer batch tx this deadlocks SQLite. Fix: teach withTxn to detect and JOIN an already-open ambient tx (preferred — no signature change; the engine's txStore already auto-threads ambient tx into engine calls), or thread ctx explicitly.
  2. publishMetaItem side-effect extraction: split into promote-only (in-tx) and side-effects (post-commit) phases; single-item publish keeps its current order (it is already effectively promote-then-effects).
  3. Contract flip: protocol-publish-package-drafts.test.ts:84-101 changes from "partial lands" to "all-or-nothing: publishedCount 0 on any failure"; the response shape keeps failed[] (now describing why the batch rolled back). Release-notes BREAKING for API consumers relying on partial publishes.
  4. Bonus fix for free: promoteDraft's best-effort draft-delete gap closes (put+delete join the same tx).

Acceptance

  • A batch where item N fails leaves items 1..N-1 absent from sys_metadata active state, no sys_metadata_commit row, registry/DDL/projections untouched.
  • Existing revert/rollback suites still green (protocol-commit-history.test.ts); memory-driver (no transaction) keeps the direct-fallthrough path with documented weaker guarantees.
  • SQLite + PG both covered: an in-tx failure test on driver-sql proves rollback; the ambient-tx join is exercised by engine-ambient-transaction.test.ts patterns.

Refs

ADR-0067 (Decision-2 is the unmet load-bearing piece; status line annotated 2026-07-16), ADR-0034 (ambient tx), ADR-0094 (one-authority + side-effect healing precedent). Code: metadata-protocol/src/protocol.ts:4562-4794, metadata-protocol/src/sys-metadata-repository.ts:238-243,577-636, objectql/src/engine.ts:2869-2896, driver-sql/src/sql-driver.ts:1400-1458.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions