Repository navigation
fix(cli)!: os migrate apply --allow-destructive holds back every organization_id drop until the organization-ownership ceremony verifies (#23684) - #23699
Conversation
…d platform-global object (red on base) Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
…failure message Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
…l the organization-ownership ceremony verifies The drop gate's table set was the platform inventory only, so --allow-destructive dropped organization_id from host-declared platform-global objects (ADR-0131 D7) and fate-supplement objects ahead of the ceremony's verified marker. The ceremony plans every table that physically carries the column, and the drift can only offer a column that is physically there, so the gate now holds back every organization_id drop the drift offers (ADR-0131 D10 item 5). Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
…rop until the ceremony verifies Claude-Session: https://claude.ai/code/session_01Q9PZ5Pzqcus4woVvTZwWNU Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 58d64ec713c7acbee42f5565f44f9e65ab01cb2c && git checkout 58d64ec713c7acbee42f5565f44f9e65ab01cb2c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3bc407d3072565e620711d8ad2a715c8922e2610 9b3d9d8daf6d5f4507db839446306bbcc8e2d9ab && git checkout -B drift-repro 3bc407d3072565e620711d8ad2a715c8922e2610 && git merge --no-ff 9b3d9d8daf6d5f4507db839446306bbcc8e2d9ab
node scripts/docs-audit/affected-docs.mjs --json 3bc407d3072565e620711d8ad2a715c8922e2610 |
Contract reviewServed-tier: The review is owed by the narrowing ( It rests on:
Nothing was built or run. ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对✅ ACCEPT: PR #23699 at
Checklist, read against GitHub and
The dev's open question (amend
Accepted deviations:
Out-of-scope, Acceptance notes, 「承接者:无」:
Landing: Generated by Claude Code |
Fixes #23684
Clause-②: no (narrowing)
What changed
os migrate apply --allow-destructivenow holds back everyorganization_iddrop that the schema-drift report offers, until the organization-ownership ceremony's marker is verified. The marker is thesys_migrationrowadr-0131-organization-ownership.packages/cli/src/utils/organization-ownership-drop-gate.ts. It held back only the tables in the platform inventory (CEREMONY_TABLES), so anorganization_iddrop on a host-declared platform-global object (ADR-0131 D7) was applied ahead of the ceremony. Now the gate holds back every one.apply.tsis unchanged. The call site and its refusal are reused as they are: the text, the--jsonenveloperefused_organization_ownership_unmigrated, exit 1, and the hold on the whole run.packages/specchange, and no second copy of the ceremony's inventory composition.The contract text that already states this refusal:
Hypothesis (a): every
organization_iddrop is the ceremony's table set (measured, holds)The ceremony's plan meets every physical base table that carries
organization_id. The table loop is inorganization-ownership-plan.ts, from:1146onc9810dd9d. Each such table gets one of four answers:--fate-supplementrow;APPLICATION_OBJECT_FATE, for any other table;uninventoried-platform-tablerefusal, for a platform-prefixed table with no row.A table without the column is counted in
outsideCeremonyand skipped.The drift can offer to drop only a column that is physically there. So on the primary database, every
organization_iddrop it proposes is on a table the ceremony plans. There, no v18 drift proposes anorganization_iddrop on a table the ceremony does not own, whether an app object or a cloud object (cloud objects are inventory placeholder rows or supplement rows). On the primary database, the rule does not over-refuse.The ceremony drops a column only through the drift.
dropColumnsThroughDriftinorganization-ownership-column-drops.ts(:38) keeps only thedrop_columnentries the drift proposed. So everyorganization_iddrop the ceremony could ever perform is one this rule holds back. That includes a fate-supplement object, whichapplycannot read.The v18 boot refusal judges the same population.
findV17ShapeTables(packages/runtime/src/organization-ownership-boot-guard.ts:139) checks every registered object withsystemFields.tenant: falseand no declaredorganization_idwhose table still carries the column; the inventory plays no part. A drop ahead of the marker on any of those tables hides the table from that refusal. That is the escape hatch D10 item 5 forbids.Where the rule is stricter than the inventory and the declarations:
systemFields: { tenant: false }over a table that still carries the column. The ceremony plans that table with the application-object fate, and the boot refusal refuses over it too.telemetrysibling datasource.applyreconciles it, but the ceremony's plan does not read it.Corpus reading on
c9810dd9d. The tenant opt-out pattern (atenantvalue of false, orsystemFieldsset to false) has 0 hits inexamples/**/*.ts, which holds 26 object files. The control is the same pattern overpackages/**/*.object.ts: 14 files. So no example app object is held by the stricter half.Hypothesis (b): the composed set is not built
The composed set would be the inventory plus the
org-scopingdeclaration, read at the call site. It would not cover the fate supplement:--fate-supplement FILE), andapplynever sees it.apply.applyflag.Rule (a) covers such an object with no flag: the drift offers its drop exactly when the ceremony itself could drop it. So (b) is not needed, and this PR returns no
needs_decision.Hypothesis (c): the existing fixture
The pins extend PR #23681's fixture file in place (
packages/cli/src/utils/organization-ownership-platform-global-column-drop.integration.test.ts), with no new rig. They reuse its host composition and its v17 template database:org-scopingprovider that declaressys_osgc_ledgerandsys_osgc_heldplatform-global;The new
describeblock runs the realos migrate apply --allow-destructive --yes --jsonthree times:Before / after (fixture; table × marker state)
c9810dd9d)sys_osgc_ledger.organization_id(host-declared platform-global)refused_organization_ownership_unmigratedsys_osgc_held.organization_id(host-declared platform-global)tables: [sys_osgc_held, sys_osgc_ledger])sys_osgc_audit.organization_id(not declared)sys_osgc_ledger.legacy_23684_note(another column; noorganization_iddrop in the run)sys_platform_setting.organization_id(platform inventory; existing pins)sys_platform_setting.organization_idTests
The tests ran on
cf15feedf4. The head9b3d9d8dafadds only the changeset file on top of it.c9810dd9dplus the pin, fix absent):Tests 1 failed | 2 passed | 3 skipped (6), withAssertionError: {"appliedOrganizationDrops":["sys_osgc_held","sys_osgc_ledger"]}: expected +0 to be 1. The two controls were green.pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2over the fixture file andsrc/commands/migrate/apply-organization-ownership-gate.integration.test.tsgaveTest Files 2 passed (2),Tests 11 passed (11).node scripts/ablation-replace.mjsput the table set back to the platform inventory only, with a restore trap.8d8ebfc14096tobea1fc2829b0, markergrep -c0 / 1 / 0.Tests 1 failed | 7 passed | 3 skipped (11)).8d8ebfc14096, andgit diff HEADwas empty.srcimports: the test, then../commands/migrate/apply.js, then../../utils/organization-ownership-drop-gate.js.pnpm --filter @objectstack/cli typecheckexited 0.tsconfig.jsonincludessrc, where the pin lives, andcheck:test-typecheckanswered OK.--project unitgave 290 files passed. Two files answered PREREQUISITE ("packages/cli is not built"); after the CLI was built they passed, 29 tests. In total, 292 of 292 files and 4325 passed + 29 skipped.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 64 commands at9b3d9d8daf. On that head, in one pass over the built workspace, all 64 exited 0. Exit codes were captured before any pipe.cf15feedf4, before the changeset commit, was missing the workspace build. There, four commands (check:dual-build-cjs-loads,check:i18n,check:i18n-coverage,check:i18n-walk-parity) exited 3, PREREQUISITE NOT MET. That is not a measurement, and it was superseded by the pass above.--ranon9b3d9d8daf:64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN.pnpm lint(eslint . --no-inline-config) on9b3d9d8daf: exit 0, no output.--format jsonover the two changed.tsfiles linted 2 files, with 0 errors and 0 warnings.Acceptance notes
content/docs/deployment/cli.mdx(:1000-:1006) says "organization_idon any table the organization-ownership ceremony covers waits for a verified … run. These tables are the ADR-0131 D7 tables and thesys_metadatafamily." The first sentence is still exact. The second names the platform's set and not, for example, an application object that opts out of the column..changeset/23429-v18-boot-refusal.md(not edited; outside this card's file surface). It describes this gate as refusing "any table in the ceremony's inventory". It also says "Only anos migrate apply --allow-destructiverun that would drop an inventory table'sorganization_idis refused". Both changesets compile into the same release, and this one's FROM/TO supersedes those two sentences. They are reported for the seat to amend before release.apply.ts(:515) reads "…, which the organization-ownership ceremony owns that drop, and it has not verified …". That is a grammar slip in an existing runtime string, reused unchanged as the ruling asked. Carrier: none.Generated by Claude Code