Skip to content

C7a (ADR-0131 D10, split from #15211): the migration inventory file, the read-only os migrate --plan, and the design of the ceremony's completion marker #22617

Description

@objectstack-fleet

Part of #15211

Filing gate ③: a task the maintainer directed. The maintainer accepted batch #310 item 2, verbatim: 「cloud 冻结在 v17 没问题,其他同意你的建议。」 (director record 6094175435 on #15211, 2026-10-10T05:22Z). C7 splits in two, and this first half starts now. Filed by the triage seat (seat post #6015, session_01AavokzJ5DndAwitDXvKy4U) as the carrier the ruling names. ⛔ Not a claim.

Reader: the domain:engine lane, with domain:cli for the command surface. The claim declares the packages/cli files under the cross-domain exception path.

Scope (C7a)

  1. The inventory file. It starts from the two measure: census the dependents of the SQL driver's orWhereNull tenant-wall carve-out before deciding its future (NULL org_id rows are globally visible on shared-DB walled deployments) #13564 censuses (59 platform objects, 28 example objects) plus the cloud supplement. Each object gets one fate, with its citation:

    • (1) column drop;
    • (2) mirror deletion;
    • (3) attribution via a parent anchor;
    • (4) report.

    sys_business_unit_member is unadjudicated in PLATFORM_OBJECT_TENANCY, so seed-replayed and system-written membership rows land organization-less #14570 (sys_business_unit_member) and plugin-sharing: after the #15030 revert, 17.x still cannot reach a NULL-org-seeded business unit from an org-stamped rule — and #14547, its only tracker, is closed #15086 (the NULL-org business unit) are read in, and each gets its fate. Re-read feat(objectql,cli): inventory + migration — four fates per object, mirrors deleted only after the id→name rewrite is verified, per-table boot report (ADR-0131 D10) #15211's pointers before writing: 6018712820, 6020279837 (three categories ruled into the plan), 6051723395, 6061638897, 6068052798, 6071418113 and 6081248392.

  2. os migrate --plan, read-only. Per table it reports the fate, the row counts, the unattributable row ids, and which tables will receive NOT NULL. It is written to a file. It refuses rather than reporting a table it cannot enumerate.

  3. The completion-marker design. This is the schema marker the ceremony's last step writes, and the one the v18 boot refusal reads (ADR-0093 D5 shape, ⛔ no env escape hatch). This card designs it and records it. C7b builds the refusal.

Not in this card (C7b, which stays on #15211)

--apply (fate order: attribution, then the verified id→name rewrite, then mirror deletion, then column drops; idempotent and resumable), the post-check, and the boot refusal. C7b stays blocked on the cutover (#15204, which now carries C2's remainder), C4 (#15205) and C5 (#15206).

Why it can start now

None of C7a depends on the data shape that C2–C5 produce. It is an inventory, a read-only plan and a design. So the critical path does not idle through the cutover window (6094175435).

Acceptance

⛔ Stop and report: any write. C7a never changes a row or a column.

Refs: ADR-0131 D10 · ADR-0093 D5 · ADR-0120 D4 · #13564 · #14570 · #15086.

Activity

  1. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 2 · 2026-10-10T06:52Z
    Session: session_01Bw3y2DWhT9RPnrmDsNqEVG
    Account: os-tesla (the seat's linked user, as get_me answers it; the card's assignee)
    Branch: claude/issue-22617-c7a-inventory-plan
    Worktree: the cloud session's own checkout (mode:cloud)
    Domain: domain:engine
    Seat: domain:engine#2 (seat post #20966)
    Provenance:

  2. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 22617,
     "status": "done",
     "branch": "claude/issue-22617-c7a-inventory-plan",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/22643",
     "session": "session_01AB6Nvx7Ue6yzJypw7VTvkj — this run's harness-stamped id; dispatched by the PM seat domain:engine#2 (session_01Bw3y2DWhT9RPnrmDsNqEVG), claim 6094844899",
     "premise_still_valid": true,
     "summary": "C7a delivered as one draft PR, #22643, with 2,011 changed lines across 7 files, under the 3,000 threshold. (1) The inventory, packages/cli/src/utils/organization-ownership-inventory.ts, gives 128 objects one D10 fate each, with a citation. (2) os migrate organization-ownership is the read-only D10 plan. Per table it gives the fate, the row counts, the ruled categories, the derivable rows per anchor, the unattributable ids with a reason, and NOT NULL readiness. It writes a plan file and never overwrites one. It refuses, naming the table, any table it cannot enumerate; an inventoried object with no table is listed as absent, never as empty. (3) The completion-marker design is in the PR body and under marker_design, with no code. Zone 2 findings: assumption 1 holds (a new subcommand; the existing plan is untouched). Assumption 2 is corrected: the census JSON on main lists 84 platform objects (49 in reach), not the 59 of #13564. The first census's 84 were read at 00d8f6541b; 3 have since left the tree and 3 are new. Its '28 example objects' are 28 files declaring 33 objects. Assumption 4 is refined: a physically absent table is a catalog fact, listed as 'absent' rather than refused, because refusing would block every deployment that does not install every plugin. A failed read, a missing column, an uninventoried platform table, an unquotable name, an unsupported dialect and a database with no inventoried platform table all refuse. The card assignee was not written.",
     "tests": "Head a3f7ab26. (a) Unit tier: OS_VERIFY_LOCK_SLOT=issue-22617 scripts/pm/os-verify-lock.sh -c 'pnpm --filter @objectstack/cli typecheck && pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2' gave 'Test Files 278 passed (278) / Tests 4115 passed (4115)', VERDICT command-exit 0. An earlier run showed 2 suites failing only on the 'packages/cli is not built' prerequisite; after pnpm --filter @objectstack/cli build they passed (29 tests). (b) Integration tier, the files the diff touches: vitest run --project integration on src/utils/organization-ownership-plan.integration.test.ts and src/utils/schema-migrate.one-shot-family.integration.test.ts, -t 'organization-ownership|ADR-0131|missing from CALLERS', gave 14 passed, VERDICT 0. That covers each fate under isolated and single, six refusals each naming its table, the write-nothing control (sha256 of the file and a full dump byte-equal, every statement a SELECT), and the family pin's no-write and no-file-created cases for the new command. The rest of the cli integration tier is declared to CI. (c) Probe: on the family pin's served database the plan planned 20 tables (9 column-drop, 11 attribution, sys_activity folded from its shard). The temporary probe line was removed with git checkout HEAD and git status was clean. (d) Ablation via scripts/ablation-replace.mjs: anchor 'if (hasPlatformObjectPrefix(base)) {' changed to '&& false'; anchor 1 to 0, blob 1787b6437ca0 to c73567cdaabc; the uninventoried pin went red ('1 failed | 9 passed'); restored, blob == HEAD and git diff HEAD empty. (e) eslint --no-inline-config on the 5 changed source and test files: exit 0. Repo-wide lint is CI's.",
     "gates": {
      "head": "a3f7ab26",
      "derived_by": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands (65 commands)",
      "results": {
       "node scripts/check-adr-0087-registration.mjs --base origin/main": 0,
       "node scripts/check-adr-0087-registration.mjs --self-test": 0,
       "node scripts/check-changeset-no-major.mjs --base origin/main": 0,
       "node scripts/check-changeset-no-major.mjs --self-test": 0,
       "node scripts/check-ci-filter-parity.mjs": 0,
       "node scripts/check-closing-keyword-parity.mjs": 0,
       "node scripts/check-closing-keyword-parity.mjs --self-test": 0,
       "node scripts/check-comment-mask-adoption.mjs": 0,
       "node scripts/check-comment-mask-adoption.mjs --self-test": 0,
       "node scripts/check-comment-mask-corpus.mjs": 0,
       "node scripts/check-dts-emitted.mjs --self-test": 0,
       "node scripts/check-empty-changeset.mjs --base origin/main": 0,
       "node scripts/check-empty-changeset.mjs --self-test": 0,
       "node scripts/check-issue-citations.mjs": 0,
       "node scripts/check-keyed-text-bounds.mjs": 0,
       "node scripts/check-keyed-text-bounds.mjs --self-test": 0,
       "node scripts/check-platform-object-tenancy-census.mjs": 0,
       "node scripts/check-platform-object-tenancy-census.mjs --self-test": 0,
       "node scripts/check-plugin-teardown-shape.mjs": 0,
       "node scripts/check-plugin-teardown-shape.mjs --self-test": 0,
       "node scripts/check-registry-log-declared.mjs": 0,
       "node scripts/check-registry-log-declared.mjs --self-test": 0,
       "node scripts/check-rest-log-spy-declared.mjs": 0,
       "node scripts/check-rest-log-spy-declared.mjs --self-test": 0,
       "node scripts/check-system-context-census.mjs": 0,
       "node scripts/check-system-context-census.mjs --self-test": 0,
       "node scripts/check-undeclared-dep-imports.mjs": 0,
       "node scripts/check-undeclared-dep-imports.mjs --self-test": 0,
       "node scripts/docs-audit/check-affected-docs.mjs": 0,
       "node scripts/docs-audit/check-drift-comment.mjs": 0,
       "node scripts/pm/release-rehearsal-clone.mjs --self-test": 0,
       "node scripts/release-pending-publish.mjs --self-test": 0,
       "pnpm --filter @objectstack/spec run check:duration-unit-keys": 0,
       "pnpm check:changeset-gate-self-tests": 0,
       "pnpm check:cross-package-test-inputs": 0,
       "pnpm check:doc-authoring": 0,
       "pnpm check:driver-memory-census": 0,
       "pnpm check:dts-closure": 0,
       "pnpm check:dual-build-cjs-loads": 0,
       "pnpm check:engine-double-contract": 0,
       "pnpm check:error-status-conformance": 0,
       "pnpm check:gitlink-declared": 0,
       "pnpm check:i18n": 0,
       "pnpm check:i18n-coverage": 0,
       "pnpm check:i18n-walk-parity": 0,
       "pnpm check:issue-citations": 0,
       "pnpm check:lean-entry-closure": 0,
       "pnpm check:logger-receiver-detach": 0,
       "pnpm check:nul-bytes": 0,
       "pnpm check:objectql-double-limit": 0,
       "pnpm check:objectui-changeset": 0,
       "pnpm check:org-identifier": 0,
       "pnpm check:page-declaration-shape": 0,
       "pnpm check:pm-changeset-deadline-census": 0,
       "pnpm check:published-files": 0,
       "pnpm check:query-options-erasure": 0,
       "pnpm check:refd-timer-probe": 0,
       "pnpm check:slot-lookup": 0,
       "pnpm check:sourcemap-no-sources-content": 0,
       "pnpm check:test-source-alias": 0,
       "pnpm check:tier-file-adoption": 0,
       "pnpm check:type-check-coverage": 0,
       "pnpm check:type-check-debt": 0,
       "pnpm check:watch-hint-literal": 0,
       "pnpm check:where-matcher": 0
      },
      "reconcile": "dispatch-gates --ran: 65 derived famil(ies) accounted for — 65 run, 0 NOT-MEASURED (a DERIVED zero — all 65 recorded an exit code and none of them is 3)",
      "earlier_reds_fixed": [
       "pnpm check:doc-authoring exit 1 on 889d4e39: tracker ids in inventory citation strings; rewritten to ADR sections and ruling-record ids",
       "pnpm check:dispatcher-error-vocabulary exit 1 on 889d4e39: unregistered code PLAN_REFUSED; class field dropped, the refusal carries reason"
      ],
      "prerequisites_cleared": [
       "check-plugin-teardown-shape --self-test exit 3 (shallow clone) cleared by fetching its pinned commit 621a4876 into refs/c7a/teardown-control",
       "check:dual-build-cjs-loads and check:i18n-coverage exit 3 (unbuilt packages) cleared by building"
      ]
     },
     "files_changed": [
      ".changeset/22617-cli-organization-ownership-plan.md",
      "packages/cli/src/commands/migrate/organization-ownership.ts",
      "packages/cli/src/utils/organization-ownership-inventory.ts",
      "packages/cli/src/utils/organization-ownership-inventory.test.ts",
      "packages/cli/src/utils/organization-ownership-plan.ts",
      "packages/cli/src/utils/organization-ownership-plan.integration.test.ts",
      "packages/cli/src/utils/schema-migrate.one-shot-family.integration.test.ts"
     ],
     "inventory_location": "packages/cli/src/utils/organization-ownership-inventory.ts. A typed TS table: the CLI ships it in dist, and the planner and the pin read one spelling. Under src/commands it would become a command (oclif pattern strategy).",
     "inventory_census": {
      "total": 128,
      "by_fate": {
       "column-drop": 38,
       "mirror-deletion": 5,
       "attribution": 71,
       "report": 14
      },
      "sources": {
       "platform-census-2026-08-31 (census record 5479883784 on #13564, at 00d8f6541b)": {
        "objects": 84,
        "column-drop": 37,
        "mirror-deletion": 5,
        "attribution": 38,
        "report": 4
       },
       "platform-census-main (scripts/platform-object-tenancy-census.json; objects added since: sys_comment_reaction, sys_flow_credential, sys_platform_setting)": {
        "objects": 3,
        "column-drop": 1,
        "attribution": 2
       },
       "example-census-2026-08-31 (the census's '28' = 28 files declaring 33 objects; unchanged on main)": {
        "objects": 33,
        "attribution": 31,
        "report": 2
       },
       "cloud-supplement (CLOUD_PROVIDED_OBJECT_NAMES; the 2026-09-03 cloud-side supplement is NOT MEASURED, it is out of this session's reach)": {
        "objects": 8,
        "report": 8,
        "cloudCarried": 8
       }
      },
      "read_in": {
       "#14570 sys_business_unit_member": "attribution: parent business_unit_id -> sys_business_unit (5536484221, 6067123924)",
       "#15086 sys_business_unit": "attribution: parent_business_unit_id -> sys_business_unit; D3 (pointer 5536478573)"
      },
      "not_measured": [
       "#13564's 2026-09-02 ledger 5507087600 answers 404 (evidence destroyed or never on this repo)",
       "the 2026-09-03 cloud-side supplement (cloud repo out of scope)"
      ],
      "cloud_carried_meaning": "Cloud stays on v17 by ruling 6094175435, so no v18 ceremony runs against a cloud database. The 8 rows carry fate 4 with cloudCarried, listed rather than dropped; C10 assigns their fates when cloud moves. If such a table ever appears on a planned database, it is enumerated and its NULL rows are reported."
     },
     "marker_design": "## The completion marker — design only (C7b builds it)\n\n⛔ This PR contains no code that writes or reads the marker. This section is the design that C7b's last step writes and the v18 boot refusal reads.\n\n**Where it lives.** One row in `sys_migration`, the deployment-level migration-flag table (`platform-objects/src/system/migration-flag.ts`, #3617). The row's primary key is a new spec constant, `ORGANIZATION_OWNERSHIP_MIGRATION_ID = 'adr-0131-organization-ownership'`, which sits beside `FILE_REFERENCES_MIGRATION_ID` and `VALUE_SHAPES_MIGRATION_ID`. No new table. `sys_migration` is itself a D7 column-drop object. Its drop runs inside the ceremony before the marker is written, so the marker is always written to the table's final, tenant-less shape.\n\n**What it records.** It reuses the existing columns:\n- `applied_at`: when the apply finished.\n- `verified_at`: when the post-check passed.\n- `blocking`: the number of tables whose post-check did not complete. This is not the count of reported rows. Reported rows are D10 fate 4: they are named at boot and do not block it.\n- `details`: one JSON document:\n  - `ceremonyVersion`: the integer `1`, the same `ceremonyVersion` this plan prints;\n  - `inventoryDigest`: the sha256 the plan prints, which ties the marker to the inventory it executed;\n  - `posture`;\n  - `tables`: per table, `{ object, fate, remainingNull, notNull: 'applied' | 'withheld', reason }`.\n\n  `verified_at` is set only by a post-check that re-ran the plan and found zero tables left mid-fate.\n\n**When it is written.** Only as the ceremony's last statement, after the post-check. An interrupted apply leaves no marker; its checkpoint lives in the ADR-0119 journal (`os migrate resume`). A fresh v18 database gets the marker when it is created, the way `attestFreshDatastore` attests the creation-attested migration ids. A database born on v18 has nothing to migrate.\n\n**How a v18 boot reads it.** This has the same fail-fast shape as ADR-0093 D5, with no escape hatch.\n- **When:** before schema sync and before any plugin `start()`. Additive sync could otherwise create tables on a database the boot is about to refuse.\n- **How:** a primary-key read of that one row, through the raw read seam, in the same pre-boot gate as the tenancy-posture refusal.\n- **What it decides:**\n  - A database with no ObjectStack tables is fresh, and is attested.\n  - A database that holds `sys_organization` but no marker row is refused, and the refusal names `os migrate organization-ownership --apply`.\n  - A marker that is unreadable, malformed, at a `ceremonyVersion` below the runtime's required version, missing `verified_at`, or with `blocking > 0` is refused.\n- **Reads fail toward refused:** the same asymmetry as `readDataMigrationFlag`.\n- **The refusal text** says what was found, that the server is refusing to start, the command to run, and that a 17.x runtime is the way to keep 17.x semantics.\n- ⛔ There is no `OS_ALLOW_*` / `OS_SKIP_*` variable and no flag that skips the read (ADR-0131 D10 item 5). A marker with tables still reporting NULL rows boots. Those tables are listed at boot with counts and the remedy (fate 4), and they stay unconstrained.\n",
     "mcp_calls": "2 — mcp__github__issue_read (get, get_comments on #22617); both reads, no write tool. Every other read went through gh api REST reads (issue and comment GETs).",
     "api_writes": "3 relay writes as objectstack-fleet[bot]: (1) pr_create POST /repos/objectstack-ai/objectstack/pulls, draft, giving #22643 (run 38036501559, body read back identical, 11374 bytes); (2) label-write --assign os-tesla, POST /issues/22643/assignees (run 38036527636, read back matches); (3) this os-dev-report comment, POST /issues/22617/comments. Plus git push, which is not a REST write.",
     "deviations": [
      "Dispatch line 3 asks for line-start 'Clause-②: yes'. The PR body and changeset spell 'Clause-②: yes (widening)', the arm AGENTS.md's closed pair allows. Line-start is kept.",
      "Zone 2.4 lists 'missing' among the refusal cases. A table physically absent from the catalog is reported as physical 'absent' with rows null, never as zero rows; refusal is kept for every unreadable case. See open_questions.",
      "No worktree under ../objectstack-issue-N was created by git worktree add -b: the outcome branch already existed in the session checkout. It was checked out into the worktree ../objectstack-issue-22617 after detaching the primary checkout, and fast-forwarded to origin/main e8c6666 before any edit."
     ],
     "open_questions": [
      {
       "question": "Should a table that is physically absent refuse the plan (Zone 2.4 lists 'missing') or be listed as absent?",
       "options": [
        "A: list it as physical 'absent' with rows null (implemented). Refuse every read that fails.",
        "B: refuse whenever an inventoried table is missing."
       ],
       "recommendation": "A. Real business need: a deployment that does not compose plugin-approvals has no sys_approval_* tables, so B would refuse forever on every such deployment. Long-term soundness: the catalog's answer is a measurement, not a guess. AI-mistake resistance: 'absent' is a distinct, typed state that is never 0. Startup scope: no new surface. The wrong-database case B guards against is refused separately ('not-an-objectstack-database')."
      },
      {
       "question": "sys_sso_provider carries better-auth's organization relation under tenancy.enabled:false. Is it outside D1, or should the column go?",
       "options": [
        "A: fate 4 report with notNullExempt (implemented): listed, never attributed or constrained.",
        "B: fate 1 column drop, which would break better-auth's provider-to-organization binding."
       ],
       "recommendation": "A, until C8 says otherwise. D1 bans a nullable TENANT column; this one is not the tenancy anchor (ADR-0066 D2), and dropping it removes a working better-auth feature."
      },
      {
       "question": "sys_notification_template: ADR §6 says its seed retires, but no column marks a seeded row.",
       "options": [
        "A: fate 3 attribution, never deleted (implemented).",
        "B: fate 2 on every row, which would delete authored templates."
       ],
       "recommendation": "A. D10 sanctions no deletion of a row that is not provably a mirror, and B would delete authored rows. C4 can add a selector if it needs one."
      }
     ],
     "out_of_scope_findings": [
      "carrier: C7b (#15211): parent chains are resolved one level in the plan; a child whose parent is attributable in the same apply is listed as unattributable with the reason 'parent row … has no organization'. Noted in the PR's Acceptance notes, not filed.",
      "carrier: none: #13564 comment 5507087600 (the second ledger, cited by ADR-0131's Evidence line) answers 404. Noted, not filed."
     ],
     "cleanup": "worktree removed after the report (node_modules first); the scratch refs refs/c7a/census1 and refs/c7a/teardown-control deleted"
    }

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT (seat review): PR #22643 at head a3f7ab26ad. C7a ships the D10 inventory and os migrate organization-ownership, a read-only plan; the marker design is recorded on #15211

    domain:engine#2 · session_01Bw3y2DWhT9RPnrmDsNqEVG (os-tesla), claim 6094844899 · 2026-10-10T08:23Z. Read against GitHub and the branch, not the report (os-dev-report 6095493258).

    Contract review at CONTRACT_REVIEW_TIER: 6095623275, PASS on this head. It is owed because of the Clause-②: yes line.

    Shape.

    • Draft, base main. Line 1 is Fixes #22617, line 2 is Part of #15211, and line 3 is Clause-②: yes (widening). That is the claim's yes, written in its explicit widening arm.
    • 7 files, +2,011/−0, all in packages/cli as the claim declared (cross-lane declaration 6095536022 on [PM seat] domain:cli — 🟢 marchtian · session_01B5CHJNXuuqzChM4w6hkTN4 #6024).
    • NOT governed (check-governed-merges). 2,011 changed lines, under the 3,000-line human-merge threshold.
    • The head merges clean with origin/main (git merge-tree).

    Acceptance, line by line

    Boundary.

    CI on a3f7ab26ad. 34 runs: 31 success, 3 skipped, 0 failure. All seven required contexts are success. The skips (Build Docs, Console Pin Gate, Packed-tarball smoke) are path or opt-in skips on the roster.

    Next: ready, then auto-merge, in this act. On merge, Fixes #22617 closes this card. #15211 stays open for C7b.

  4. objectstack-fleet commented on Oct 10, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #22643 → cc305a3cfc through the merge queue, at 2026-10-10T08:42Z. domain:engine#2 · session_01Bw3y2DWhT9RPnrmDsNqEVG · 2026-10-10T08:44Z.

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

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratedomain:engineenhancementNew feature or requestpriority:p1High: required for production / M2target:v18

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions