Skip to content

fix(cli): os migrate plan names the ownership ceremony, not --allow-destructive, for the organization_id drops apply refuses - #24954

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-24925-plan-names-ceremony
Oct 20, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/issue-24925-plan-names-ceremony

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24925
Clause-②: no

os migrate plan no longer offers --allow-destructive for the organization_id drops that os migrate apply refuses ahead of the organization-ownership ceremony (ADR-0131 D10 item 5). On a database the ceremony has not verified, each such drop is still listed and counted as drift. Its line names the ceremony as the remedy, and the footer names the ceremony's two commands. A verified database, or one with no organization_id drop, gets the plan it got before, byte for byte. The --json document, the exit code and os migrate apply are unchanged.

What changed

  • packages/cli/src/commands/migrate/plan.ts
    • ceremonyHeldDrops answers which entries are held. It uses the two functions apply's drop gate asks: ceremonyOrganizationColumnDrops, asked one entry at a time, and readOrganizationOwnershipMarker, read through the stack's objectql service as apply reads it. There is no second inventory and no table list. The marker is read only when such a drop is listed, and never under --json.
    • A held drop's line is the differ's finding without its --allow-destructive sentence (driftFindingWithoutRemedy, the seam [#23925] added), then: a drop the organization-ownership ceremony owns, and it has not verified on this database: the ceremony drops it itself (see below).
    • The footer while a drop is held:
      • Apply with: os migrate apply prints only when that command has non-destructive or additive work. It never adds --allow-destructive.
      • Then one warning counts the held drops and the other destructive changes that wait for the ceremony. It names os migrate organization-ownership --out PLAN, then os migrate organization-ownership --apply --plan-file PLAN --backup-taken, spelled as apply's refusal spells them.
      • A driver's [#23925] apply limit keeps its own warning, and its count excludes held drops.
  • packages/cli/src/commands/migrate/plan.apply-limit.test.ts
    • Eight pins, listed below. They are folded into the neighbour so the suite pays one module load, not two.
    • The harness's stderr spy now runs the write callback. Each case no longer waits out the 2 s drain timeout in utils/one-shot-exit.ts: the file's 12 cases took 24 to 75 ms each after the first, on a shared box.
  • .changeset/24925-plan-names-ownership-ceremony.md: @objectstack/cli patch.

One deviation from the suggested route, measured

The suggested route kept offering --allow-destructive for the other destructive entries. But apply's gate refuses the whole --allow-destructive run while any organization_id drop is in it. Case 1c of apply-organization-ownership-gate.integration.test.ts pins that, and step 1 below shows it again: an unrelated sys_sso_provider.created_by drop was not applied either. So apply would perform none of the destructive changes. The footer offers the flag for none of them, and names the rest as waiting for the ceremony.

Measured

Setup:

  • Database. An app from create-objectstack@17.7.0 (npm), booted once on 17.7.0 with objectstack dev --seed-admin. That leaves 43 tables carrying organization_id, no ownership marker, sys_comment.reactions present and 0 comments.
  • How it was run. This repository's CLI built at feb09db14 (= main), and then this branch. It ran from a minimal project directory with OS_AUTH_SECRET set, so the auth family composes, against copies of that SQLite file.
  1. The data path is held.
    • A plain os migrate apply --allow-destructive --yes exits 1 at the reactions gate first, as in the card's walk.
    • After os migrate comment-reactions --apply --yes (Converted 0 reaction(s) … recorded as verified), os migrate apply --allow-destructive --yes exits 1: ✗ Refusing this --allow-destructive run: it would drop organization_id from sys_job, sys_job_queue, sys_metadata, sys_metadata_commit, sys_metadata_history, sys_migration, sys_migration_journal, sys_secret, a drop the organization-ownership ceremony owns, and the ceremony has not verified on this database. … Nothing in this run was applied.
    • Column reads before and after match: 45 tables carry organization_id in both; sys_job, sys_metadata and sys_secret keep it; sys_sso_provider.created_by is still there.
  2. Main's plan. It prints 28 change(s): 6 safe, 0 needs-confirm, 22 destructive. Each organization_id drop reads … (orphaned) — "os migrate apply --allow-destructive" to drop it., the footer reads Apply with: os migrate apply (add --allow-destructive for drops / tightenings), and the ceremony is never named.
  3. No sys_migration table (a copy with DROP TABLE sys_migration). The branch's plan exits 0 and lists the 7 remaining drops as held. It prints no WARN or ERROR line beyond the 3 boot WARN lines the held copy prints too.
  4. This branch, on the same database. The 8 drops carry the ceremony line, and the footer is:
  ℹ 28 change(s): 6 safe, 0 needs-confirm, 22 destructive
  Apply with: os migrate apply
  ⚠ 8 organization_id drop(s) above are the organization-ownership ceremony's, and it has not verified on this database, so "os migrate apply --allow-destructive" refuses every run until it has (the other 14 destructive change(s) above wait for it too). Run "os migrate organization-ownership --out PLAN" to write and review its plan, take a backup, then "os migrate organization-ownership --apply --plan-file PLAN --backup-taken"; it drops these columns itself.

Byte for byte. Main's plan.ts and this branch's, each built on the same tree, ran over the same four databases. Log timestamps and the duration line were normalised before comparing.

database main vs branch
17.x, no marker differs on the 8 held entry lines and the footer only (19 diff lines)
17.x plus a verified marker row identical
17.x with no sys_migration table differs on the 7 held entry lines and the footer only (17 diff lines)
a fresh 18 database (birth marker removed) plus one orphan column, no organization_id drift identical

Pins: red on main, then green; one ablation

Pins in plan.apply-limit.test.ts. bootSchemaStack is stubbed, and the marker read is the real readOrganizationOwnershipMarker over a fake engine.

  • Held, five cases. No marker row; an unverified row; no sys_migration table, where the read throws and the plan answers "not verified"; nothing apply can do before the ceremony, so there is no apply line; and beside a driver's declared apply limit.
  • --json. The document is unchanged, and no marker is read.
  • Two controls. A verified marker gives today's plan, and the marker WAS read. No organization_id drop gives today's plan, and the marker is never read.
leg result
plan.ts at feb09db14 (main), tree only, restored after 6 failed, 6 passed. The 5 held cases fail on their entry lines. The verified control fails only at its final read-count assertion: main never reads the marker, and its plan-output assertions above that line passed.
branch 12 passed
ablation: return marker.verified ? … changed to return true ? … (every database treated as verified), via scripts/ablation-replace.mjs 5 failed (the five held cases), 7 passed. Restored: blob 03cb80830b10 equals HEAD's, and git diff HEAD is empty.

The subject resolves through relative source imports (./plan.js), not through a package's exports, so no dist/ preflight applies.

Local verification at 003e41ab51

  • pnpm --filter @objectstack/cli typecheck exits 0, test layer included: check:test-typecheck: OK.
  • pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2 exits 0: Test Files 315 passed (315), Tests 4650 passed (4650).
  • The integration tier is left to CI: this diff touches no integration file, spawn entry or driver boot path.
  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 67 derived commands, plus the 11 declared WIDE-population families. All 78 exit 0.
    • Three first exited 3 or 1 and were re-run on the same head. check:dual-build-cjs-loads and check:i18n-coverage printed PREREQUISITE NOT MET and COULD NOT MEASURE until the unbuilt packages they read were built. check:route-envelope hit an ENOENT on a temporary tsup file my concurrent build was writing.
    • --ran: ✓ dispatch-gates --ran: 67 derived famil(ies) accounted for — 67 run, 0 NOT-MEASURED (a DERIVED zero — all 67 recorded an exit code and none of them is 3).
  • pnpm lint (eslint . --no-inline-config) on this head exits 0 with no findings printed.

Acceptance notes

  • Ordering, measured.
    • On the pristine 17.x copy, the ceremony run straight (--out, then --apply … --backup-taken) stops at its rewrite step: sys_user_permission_set has no permission_set column, so no grant can name its set. Boot this release once against the database (its schema sync adds the column), then re-run.
    • After os migrate apply without the flag (exit 0, 16 tables created or extended, 6 changes applied), the same ceremony passes rewrite.
    • A refused os serve boot adds that column too.
    • So the plan's Apply with: os migrate apply line above the ceremony line is a sanctioned first step, not a detour. In this minimal composition the ceremony then stopped at column-drops on sys_flow_credential, an object my measurement project does not declare. That is an artefact of the measurement set-up, not of this change.
  • Not changed here (held off by the claim), for the PM to route.
    • The boot's [schema-drift] lines in driver-sql still say "os migrate apply --allow-destructive" to drop it for these columns. Measured: os serve on a 17.x copy exits 1 after 22 [schema-drift] lines; the boot guard's refusal that follows names the ceremony.
    • os migrate apply without the flag still prints 22 destructive change(s) will be SKIPPED (re-run with --allow-destructive to include them). and Skipped 22 destructive change(s) (re-run with --allow-destructive to include them)., which is a re-run its own drop gate refuses (apply.ts).
    • The plan still offers --allow-destructive for sys_comment.reactions before os migrate comment-reactions has run, though apply refuses that drop too. Its predicate is inline in apply.ts, with no exported function to share, so it is not folded in here.
  • The ceremony's two commands are spelled in plan.ts and, separately, in apply.ts's refusal (held off). No shared constant was introduced.
  • Read, not measured: the boot guard also refuses a verified marker of an older shape generation (outdated). apply's drop gate, and so this plan, ask only isDataMigrationFlagVerified. The plan follows apply here by construction.

Generated by Claude Code

…zation_id drops apply refuses

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
…ps, with verified and no-drop controls

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
… one module load for both

Claude-Session: https://claude.ai/code/session_01LmjU2x1faLZ39DVTFyVca6
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 4 documentable anchor(s).

7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/data-modeling/indexing.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/deployment/cli.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/deployment/index.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/kernel/services-checklist.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/protocol/kernel/lifecycle.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/upgrading.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))

⛔ 7 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-3.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-4.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-5.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-6.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v17/17-7.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v18/18-0.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 30 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 865d2c8cf4accaaa5d1885d512f694e4abdce440 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4920f390e1b5c18e7921ac038bb41fd438b5df86 — the merge of head 003e41ab5135d4db35636dcae23c5ab7b1d2d343 into base 865d2c8cf4accaaa5d1885d512f694e4abdce440, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4920f390e1b5c18e7921ac038bb41fd438b5df86 && git checkout 4920f390e1b5c18e7921ac038bb41fd438b5df86
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 865d2c8cf4accaaa5d1885d512f694e4abdce440 003e41ab5135d4db35636dcae23c5ab7b1d2d343 && git checkout -B drift-repro 865d2c8cf4accaaa5d1885d512f694e4abdce440 && git merge --no-ff 003e41ab5135d4db35636dcae23c5ab7b1d2d343

node scripts/docs-audit/affected-docs.mjs --json 865d2c8cf4accaaa5d1885d512f694e4abdce440

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 865d2c8cf4accaaa5d1885d512f694e4abdce440 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 · ACCEPT

domain:cli seat 2 (#22648) · os-justin · session session_01LmjU2x1faLZ39DVTFyVca6 · 2026-10-20T18:37Z · head 003e41ab5135d4db35636dcae23c5ab7b1d2d343 · claim 6296978345 → report 6298327441. No contract face (Clause-②: no, the human face of os migrate plan), so no review of record is owed.

Check Reading
Shape Draft, base main. First line Fixes #24925; Clause-②: no line-initial. Assignee os-justin = the card's. One changeset, @objectstack/cli: patch.
Surface 3 files, +342 / −11: plan.ts, its pin file plan.apply-limit.test.ts, and the changeset, inside the claim. The drop gate, apply.ts, driver-sql's schema-drift.ts, the guard and the ceremony are untouched. check-governed-merges.mjs --pr 24954: NOT governed.
Mechanism Read in the diff. ceremonyHeldDrops asks the drop gate's own ceremonyOrganizationColumnDrops, one entry at a time, and readOrganizationOwnershipMarker through the stack's objectql service, the way apply does. The marker is read only when such a drop is listed. Held drops stay listed and counted, with driftFindingWithoutRemedy (a driver-sql export already on main) plus the ceremony as their remedy. The footer offers --allow-destructive for nothing while a drop is held, and names the ceremony's two commands. The --json branch emits before this code (plan.ts:270), so its document is unchanged.
Evidence The data path was measured held first: after comment-reactions --apply, apply --allow-destructive exits 1 with the drop gate's refusal and drops nothing. 6 of 12 pins are red on main. Ablating the marker read turns the 5 held cases red; it was restored hash-equal. On four SQLite copies from a real create-objectstack@17.7.0 database: a verified marker and a fresh 18 database print plans byte-identical to main, and with no sys_migration table the run exits 0 with only the held lines changed.
Gates Local: 67/67 derived families and the 11 Declared WIDE population families exit 0, 0 UNRUN; pnpm lint exit 0; the CLI unit project in full, 315 files / 4650 tests. Check-runs on this head (38, latest per name), read at this act: 18 success, 7 skipped, 13 in_progress. Landing waits on all of them.

Accepted deviations:

  • The footer offers --allow-destructive for no entry while a drop is held, not "for the rest". apply's gate refuses the whole run (measured), so Zone 2's own criterion yields zero.
  • The pins are folded into the existing pin file.
  • main was not merged; the six newer commits touch none of the paths.
  • The real-database behaviour is measured on copies and pinned at unit level.

Out-of-scope dispositions (one line each):

Landing: through the merge queue by this seat once every check on this head reads success (or an expected skip).


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 20, 2026 19:01
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 20, 2026 19:02
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit 5f11cec Oct 20, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24925-plan-names-ceremony branch October 20, 2026 19:27
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 size/m tests tooling

Projects

None yet

2 participants