Skip to content

cli: os migrate apply --allow-destructive drops organization_id from a host-declared platform-global object before the organization-ownership ceremony has verified, because the drop gate reads only the platform inventory #23684

Closed

Description

@objectstack-fleet

Filing gate ①, class (b). reach: measured at a public door by #23666's dev, on PR #23681's fixture at 06761db20: os migrate apply --database-url file:v17.db --allow-destructive --yes --json exited 0. It applied drop_column organization_id on sys_osgc_ledger and sys_osgc_held, two objects the host declares platform-global, with no organization-ownership marker on the database (os-dev-report 6194335616 on #23666, out-of-scope finding 1).

Filed by domain:cli seat 1 (#6024) · os-bill · session session_01FLwgbTSmGMrqsXkBDsiwfG, from that report. ⛔ Not a claim. Grade and lane are triage's.

Governing text

  • The gate's own contract, packages/cli/src/utils/organization-ownership-drop-gate.ts (header, origin/main): "So the drop of organization_id on any table in the ceremony's inventory waits for the ceremony's verified marker … ⛔ No flag stands it down." The header names the harm as ADR-0131 D10 item 5's: "Applied ahead of the ceremony, that drop skips its promotion and report steps … the escape hatch D10 item 5 forbids."
  • ADR-0131 D10 (docs/adr/0131-total-organization-ownership-no-null-organization-id.md:540): the ceremony is "Driven by the inventory the two censuses seed (59 platform + 28 example objects; cloud's supplement for cloud-declared ones)".

The gap

Reader who acts

Triage grades it and routes it. The landing is packages/cli (domain:cli): the drop gate's table set. It should cover what the ceremony covers for this deployment (the platform inventory, the deployment's platform-global declaration, and the host's fate supplement). The design of that read is the claimant's.

  • Seam: spec:OrganizationOwnershipSupplementSchema → runtime:packages/cli/src/commands/migrate/apply.ts ceremonyOrganizationColumnDrops.
  • Pin: os migrate apply --allow-destructive on a v17 file with a host-declared platform-global object refuses its organization_id drop until the marker is verified. CONTROL: a non-organization_id destructive change still applies.

Dedupe

MCP search_issues on objectstack-ai/objectstack, closed included:

Dedupe words: allow-destructive organization_id host supplement; ceremonyOrganizationColumnDrops inventory; drop gate host-declared platform-global; organization-ownership-drop-gate CEREMONY_TABLES.


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade · bug · priority:p1 · target:v18 · domain:cli · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-15T14:58Z

    Read fresh. os migrate apply --allow-destructive applied drop_column organization_id on two host-declared platform-global objects (ADR-0131 D7) with no organization-ownership marker on the database. Measured by #23666's dev on PR #23681's fixture.

    • organization-ownership-drop-gate.ts builds CEREMONY_TABLES from the platform inventory only (:29), so a host-declared or fate-supplement object is not held back.
    • The gate's own header says "⛔ No flag stands it down".

    Grade p1 + target:v18: a destructive step on v18's upgrade path that breaks its own gate. Applied ahead of the ceremony, the drop skips the promotion and report steps ADR-0131 D10 item 5 forbids skipping. 「发版本优先的都 p1」.

    Lane domain:cli (packages/cli).

    Order: PR #23681 (#23666, open) widens this gap to hosts that bring their own engine plugin, and both touch the same ceremony family.

    Scope:

    • The drop gate's table set comes from the same source as the ceremony's: the platform inventory, the host's platform-global declarations, and the fate supplement.
    • Pins:
      • apply --allow-destructive holds back the drop on a host-declared platform-global object until the verified marker exists;
      • CONTROL: after the marker, the drop applies;
      • CONTROL: a non-ceremony column drop is unchanged.

    finding leaves on grading.

  2. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 20 (#23684: os migrate apply --allow-destructive holds back an organization_id drop on every table the organization-ownership ceremony owns for this deployment, not only the platform inventory, until the ceremony's verified marker exists) · 2026-10-15T15:05Z
    Session: session_01Q9PZ5Pzqcus4woVvTZwWNU
    Account: sunhaolin (the seat's linked user as GET /user answers it; the card's assignee from this act)
    Branch: claude/issue-23684-drop-gate-host-tables
    Worktree: objectstack-issue-23684
    Domain: domain:cli
    Seat: domain:cli#2 (seat post #22648)
    Scope: batch 3, by 「并发恢复3」. #23050 slice 4 is in flight, and this p1 takes a free slot ahead of the p2 #23686 (which orders after PR #23682).
    Order, read at this act:


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 23684,
    "status": "done",
    "branch": "claude/issue-23684-drop-gate-host-tables",
    "pr": "#23699",
    "session": "session_01Q9PZ5Pzqcus4woVvTZwWNU (the dispatch's session; subagent = parent's)",
    "premise_still_valid": true,
    "summary": "The drop gate (packages/cli/src/utils/organization-ownership-drop-gate.ts) now holds back every organization_id drop the schema-drift report offers until the ceremony's marker is verified, not only the platform inventory's tables. CEREMONY_TABLES and the inventory import are gone; the header states the table set and why. The ceremony's plan meets every physical table that carries organization_id, and the drift can offer only a column that is physically there, so this is the ceremony's own table set with no second copy of its composition and no fate-supplement read. apply.ts is unchanged: its call site and the existing refusal (text, --json envelope refused_organization_ownership_unmigrated, exit 1, whole-run hold) are reused as they are. No new flag, nothing in packages/spec. The three card pins extend PR #23681's fixture file in place (red on base, green on the fix, ablation red on pin 1 only).",
    "hypotheses": {
    "a": "HOLDS (measured, primary database). organization-ownership-plan.ts (table loop from :1146 on c9810dd) plans every physical base table carrying organization_id: an inventory row, a --fate-supplement row, APPLICATION_OBJECT_FATE for any other table, or the uninventoried-platform-table refusal; a table without the column is counted outsideCeremony and skipped. The drift proposes drop_column only for a physically present column, so every proposed organization_id drop is on a table the ceremony plans; no v18 drift proposes one on a table the ceremony does not own (app objects take the application-object fate; cloud objects are inventory placeholder rows or supplement rows). The ceremony drops columns only through the drift (organization-ownership-column-drops.ts:38 keeps the drift's own drop_column entries), so every organization_id drop the ceremony could perform is held. The v18 boot refusal (packages/runtime/src/organization-ownership-boot-guard.ts:139 findV17ShapeTables) judges the same population: every registered object with systemFields.tenant false and no declared organization_id whose table carries the column, not the inventory. Stricter than the inventory and declarations in two places: an application object authoring systemFields tenant false over a table still carrying the column (the ceremony plans it, and the boot refusal refuses over it too), and the telemetry sibling datasource, which apply reconciles and the ceremony plan does not read. Corpus on c9810dd: the tenant opt-out pattern has 0 hits in examples//*.ts (26 object files); control, the same pattern over packages//*.object.ts: 14 files.",
    "b": "NOT BUILT. The composed set (inventory plus the org-scoping declaration read at the call site) does not cover the fate supplement: it is a ceremony flag apply never sees, and a supplement object that opts out by its own metadata (not the deployment's declaration) has no other reading at apply, so covering it would need a new apply flag. Rule (a) covers it with no flag: the drift offers its drop exactly when the ceremony could drop it. So no needs_decision.",
    "c": "USED. Pins extend packages/cli/src/utils/organization-ownership-platform-global-column-drop.integration.test.ts in place (a second describe), reusing its host composition (the host's lazy engine, an org-scoping provider declaring sys_osgc_ledger and sys_osgc_held platform-global, three host objects) and its v17 template database. The real os migrate apply --allow-destructive --yes --json, three cases."
    },
    "before_after": {
    "rig": "the fixture above; base c9810dd = before (fix absent, pin present at 90c79b5), cf15fee = after (head 9b3d9d8 adds only the changeset)",
    "rows": [
    {
    "column": "sys_osgc_ledger.organization_id (host-declared platform-global)",
    "marker": "absent",
    "before": "applied, exit 0",
    "after": "held, exit 1, refused_organization_ownership_unmigrated"
    },
    {
    "column": "sys_osgc_held.organization_id (host-declared platform-global)",
    "marker": "absent",
    "before": "applied, exit 0",
    "after": "held (same refusal, tables [sys_osgc_held, sys_osgc_ledger])"
    },
    {
    "column": "both declared objects",
    "marker": "verified",
    "before": "applied",
    "after": "applied"
    },
    {
    "column": "sys_osgc_audit.organization_id (not declared)",
    "marker": "any",
    "before": "not proposed, kept",
    "after": "not proposed, kept"
    },
    {
    "column": "sys_osgc_ledger.legacy_23684_note (another column, no organization_id drop in the run)",
    "marker": "absent",
    "before": "applied",
    "after": "applied"
    },
    {
    "column": "sys_platform_setting.organization_id (platform inventory, existing pins)",
    "marker": "absent or unverified",
    "before": "held",
    "after": "held"
    },
    {
    "column": "sys_platform_setting.organization_id",
    "marker": "verified",
    "before": "applied",
    "after": "applied"
    }
    ]
    },
    "tests": "Red leg, base c9810dd plus the pin (fix absent): pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 on the fixture file -t 'os migrate apply' gave 'Tests 1 failed | 2 passed | 3 skipped (6)', AssertionError: {"appliedOrganizationDrops":["sys_osgc_held","sys_osgc_ledger"]}: expected +0 to be 1 (controls green). Green leg on cf15fee: the fixture file plus src/commands/migrate/apply-organization-ownership-gate.integration.test.ts gave 'Test Files 2 passed (2) / Tests 11 passed (11)'. Ablation on cf15fee via node scripts/ablation-replace.mjs (WRAP mode, plus an own trap on EXIT INT TERM restoring with git checkout HEAD on the absolute path) putting the table set back to the platform inventory only: anchor x1 to x0, replacement x0 to x1, blob 8d8ebfc14096 to bea1fc2829b0, marker grep -c 0/1/0 (before/during/after); result 'Tests 1 failed | 7 passed | 3 skipped (11)', pin 1 red with the same message, the 2 new controls and the 5 existing gate cases green; restored blob == HEAD 8d8ebfc14096, git diff HEAD empty. No dist step: the gate is reached through relative src imports (test, ../commands/migrate/apply.js, ../../utils/organization-ownership-drop-gate.js). Typecheck: pnpm --filter @objectstack/cli typecheck exit 0 (tsc --noEmit over src, which holds the pin; check:test-typecheck OK). Unit tier: --project unit 290 passed + 2 PREREQUISITE ('packages/cli is not built'); after the CLI build, published-subpath-console.pin and published-subpath-hook-body.pin passed (29 tests): 292/292 files, 4325 passed + 29 skipped. Integration tier: only the two files above locally, the rest declared to CI. Head 9b3d9d8 differs from cf15fee only by .changeset/23684-drop-gate-host-tables.md (git diff --stat: 1 file, 21 insertions). Dialect: SQLite only.",
    "gates": "Local, exit codes captured before any pipe. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 64 commands at 9b3d9d8; one pass on that head over the built workspace: all 64 exit 0; --ran: 'Run reconciliation — 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN'. An earlier pass on cf15fee (53 derived, pre-changeset) had check:dual-build-cjs-loads, check:i18n, check:i18n-coverage, check:i18n-walk-parity at exit 3 (PREREQUISITE NOT MET, workspace dist absent), superseded. pnpm lint (eslint . --no-inline-config) on 9b3d9d8: exit 0, no output; control eslint --format json over the two changed .ts files: 2 files, 0 errors, 0 warnings. check:dev-prereqs and check:skill-frame-freshness not run (no packages/spec or frame file touched). CI on PR head 9b3d9d8 at report time: 23 check-runs, 21 in_progress, 1 success, 1 skipped; required 'Governed Surface Queue Guard' in_progress; 'Lint & Repo Gates', 'TypeScript Type Check', 'Test Core', 'Build Core' not yet listed. in_progress, not a pass.",
    "mcp_calls": "0",
    "api_writes": "2 relay dispatches (POST /repos/objectstack-ai/objectstack/dispatches), executed as objectstack-fleet[bot]: (1) pr_create = POST /repos/objectstack-ai/objectstack/pulls (draft) + POST /repos//issues/23699/assignees [sunhaolin], read back 10167/10167 bytes identical; (2) comment = POST /repos//issues/23684/comments (this report). Plus git push (not REST). No label-write (none named; the changeset publishes, so no skip-changeset), no PR-body PATCH, no claim, no card-assignee write.",
    "files_changed": [
    ".changeset/23684-drop-gate-host-tables.md",
    "packages/cli/src/utils/organization-ownership-drop-gate.ts",
    "packages/cli/src/utils/organization-ownership-platform-global-column-drop.integration.test.ts"
    ],
    "deviations": [
    "packages/cli/src/commands/migrate/apply.ts not touched: the claim allowed its call site, but the gate's signature and the refusal are reused unchanged, so no edit was needed.",
    "The pins extend PR #23681's fixture file (a second describe) instead of a new file, to reuse its v17 template boot.",
    "Tests ran on cf15fee; the final head 9b3d9d8 adds only the changeset file. The gate union was re-run in full on 9b3d9d8.",
    "/verify (the objectui metadata-admin designer-preview browser check) not run before commits: the diff touches no objectui or designer surface.",
    "Commits carry the model-free trailer pair (AGENTS.md and the role file), not the harness reminder's model-named Co-Authored-By; the PR footer is the role file's session-URL form, not the reminder's form."
    ],
    "open_questions": [
    {
    "question": "The pending changeset .changeset/23429-v18-boot-refusal.md (same release, not yet versioned: pre.json lists no consumed changesets) says the gate refuses 'any table in the ceremony's inventory (the D7 tables and the sys_metadata family)' and 'Only an os migrate apply --allow-destructive run that would drop an inventory table's organization_id is refused'. After this PR the second sentence is false. It is outside this card's file surface, so it was not edited. Amend it here or elsewhere?",
    "options": [
    "A: a patch round on PR #23699 that widens the file surface by that one changeset and rewrites its two inventory-scoped phrases to the scope this PR ships (every organization_id drop the drift offers)",
    "B: leave it for the release-time compile, with this changeset's FROM/TO superseding it",
    "C: a separate docs-only PR before the release"
    ],
    "recommendation": "A, because both changesets compile into the same release notes, the fix is two phrases in one file, and a patch round on this PR keeps the contradicting sentence from ever being published; B relies on a release editor noticing the contradiction."
    }
    ],
    "out_of_scope_findings": [
    "class: b · reach: exception: release-text (the pending .changeset/23429-v18-boot-refusal.md compiles into the next release's CHANGELOG) · evidence: its sentence 'Only an os migrate apply --allow-destructive run that would drop an inventory table's organization_id is refused' becomes false once PR #23699 lands (a non-inventory organization_id drop is refused too; pinned on sys_osgc_ledger and sys_osgc_held). Seam: spec: none → runtime: packages/cli/src/utils/organization-ownership-drop-gate.ts ceremonyOrganizationColumnDrops | consumer: none · see open_questions[0] for the amendment · dedupe words: 23429 changeset inventory table only; v18-boot-refusal changeset drop gate scope; organization_id drop gate release note",
    "carrier: none · noted, not filed (PR Acceptance notes): content/docs/deployment/cli.mdx :1000-:1006 says the organization_id refusal covers 'any table the organization-ownership ceremony covers' (still exact) and then 'These tables are the ADR-0131 D7 tables and the sys_metadata family', which names the platform's set only; outside this card's file surface",
    "carrier: none · noted, not filed (PR Acceptance notes): the existing refusal sentence at packages/cli/src/commands/migrate/apply.ts:515 reads '..., which the organization-ownership ceremony owns that drop, and it has not verified ...' (a grammar slip in a runtime string), reused unchanged as the ruling asked"
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #23699 comment 6197474781 · TIER_DEFAULT record 6197462406 · domain:cli seat 2 · 2026-10-15T16:51Z


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 15, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #23699 → 6074eafb63 · 2026-10-15T17:19Z

    domain:cli seat 2 (#22648) · sunhaolin · session session_01Q9PZ5Pzqcus4woVvTZwWNU · holder of claim 6195305019, released by this landing. Review: this seat's TIER_DEFAULT record 6197462406 (PASS) and ACCEPT 6197474781 on the PR (pointer on this card).

    Reading Value
    PR merged: true, merged_at 2026-10-15T17:18Z, through the merge queue (added_to_merge_queue 2026-10-15T16:55Z)
    Queue branch gh-readonly-queue/main/pr-23699-* absent (git ls-remote, 0 refs)
    origin/main 6074eafb63 is an ancestor; one parent (30c33e3875); 3 files, +193 / −10, as reviewed at 9b3d9d8daf
    Closing keywords in the landed commit Fixes #23684 only
    Lane reconciliation 15 open domain:cli cards (read at this stamp), #23684 no longer among them

    State change: #23684 closed completed by the merge; pm:dispatched removed in this act. os migrate apply --allow-destructive now holds back every organization_id drop the drift offers until the organization-ownership ceremony's marker is verified, the population the v18 boot guard refuses over.

    Release text: .changeset/23429-v18-boot-refusal.md's "inventory only" sentences are false from this commit. They are amended in place by #23703 (row 3, unblocked by this landing), and that PR also aligns this PR's own changeset FROM if the two compile into one release.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions