Skip to content

fix(cli): os migrate organization-ownership plans a v17 database still holding the retired sys_scim_provider, as fate 4 (ADR-0131 D10) - #23874

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/sharp-matsumoto-1ec135
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/sharp-matsumoto-1ec135

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Clause-②: no

What was wrong

os migrate organization-ownership refuses a v17 database that still holds the retired sys_scim_provider table:

sys_scim_provider carries organization_id but the ADR-0131 D10 inventory gives it no fate.
  • Why the inventory had no row. sys_scim_provider was retired from the tree on 2026-08-28. The ADR-0087 entry is packages/spec/src/migrations/entries/semantic/18.scim-provider-object-retired.ts. The first census was measured on 2026-08-31, so neither census counted the object, and the inventory got no row for it.
  • Why the table is still there. The same ADR-0087 entry leaves the table in deployed databases "in place untouched, by ruling — no backfill, no reaper, no migrate command". So every database that booted an earlier release still holds it, organization_id included.
  • What the plan does with it. It throws uninventoried-platform-table for any platform-prefixed table that carries the column and has no row (organization-ownership-plan.ts, the loop in buildOrganizationOwnershipPlan). --apply builds the same plan, so the ceremony cannot run on such a database at all. os dev / os serve then stay refused, because the boot guard waits for the ceremony's marker.
  • Where it was measured. A copy of the maintainer's local hotcrm dev.db, a long-lived SQLite dev database first created on 17.x, on @objectstack/* 18.0.0-next.2. The plan refused it. A one-row host fate supplement for sys_scim_provider was the workaround there.

What changed

  • A new census population, retired-before-census. It holds platform objects retired from the tree before the first census, whose tables an upgraded database still holds. It is documented in the inventory's header with the other populations.

  • A fate-4 row for sys_scim_provider. It follows the precedent of sys_view_definition, sys_saved_report and sys_report_schedule (fate report: rows listed, never attributed, never deleted). Its citation names the ADR-0087 entry and carries no tracker number.

  • notNullExempt on the row. The precedent rows do not carry one; this row does, for three reasons:

    • the ruling leaves this table untouched;
    • no declaration anchors tenancy on its column any more;
    • the retired object declared organization_id as required: false, because better-auth's SCIM plugin scopes a connection to an organization only optionally. A connection with no organization was a legitimate row.

    Without the exemption, the plan would report that the table "will receive NOT NULL" once no NULL row remains, which is a promise about a table the ruling says nothing touches.

Pins

  • organization-ownership-inventory.test.ts. A new frozen list, RETIRED_BEFORE_CENSUS (sys_scim_provider → scim-provider-object-retired). For each entry, the test asserts:
    • the population's premise: it is in neither census list and not in PLATFORM_OBJECTS_BY_PACKAGE;
    • the ADR-0087 entry the citation names exists in MIGRATIONS_BY_MAJOR, and its acceptance criteria still say the table is "left in place untouched", which is what licenses fate 4;
    • the row has census retired-before-census, fate report, a citation containing the entry id and no tracker number, and a notNullExempt;
    • control: no other row claims the population.
  • organization-ownership-plan.integration.test.ts. A v17-shaped sys_scim_provider (its last declared columns: id, provider_id, provider_key, scim_token, organization_id, user_id, created_at, updated_at), with one stamped row and one NULL row.
    • Under isolated and single, it plans as source: inventory, fate report.
    • Counts: 2 total / 1 NULL / 1 stamped.
    • The NULL row is listed as "fate 4: reported, never guessed". Under single it is not given to the Default Organization.
    • notNull.willReceive: false, with the exemption's reason.
    • Control: the same database, planned with the inventory minus this row, is refused uninventoried-platform-table naming sys_scim_provider.

Verification

On b0aca327bf (base origin/main f7e836da1e):

  • pnpm --filter @objectstack/cli typecheck: exit 0.
  • The two touched files (vitest run src/utils/organization-ownership-inventory.test.ts src/utils/organization-ownership-plan.integration.test.ts): Test Files 2 passed, Tests 37 passed.
  • pnpm --filter @objectstack/cli test (both projects):
    • First run: Test Files 13 failed | 430 passed | 1 skipped (444), Tests 11 failed | 5694 passed | 51 skipped. None of the 13 is an ownership-ceremony file. Five of them refused to load with "packages/cli is not built", because the run had built only the CLI's dependencies.
    • After pnpm exec turbo run build --filter=@objectstack/cli, the 13 re-run as 7 failed | 6 passed, 14 failed tests.
    • Clean-base control. The three changed source files were restored to the base f7e836da1e, the CLI was rebuilt, and those 7 re-run. The failing set is identical, test by test: 7 failed (7), 14 failed. The tree was then restored to HEAD and the CLI rebuilt.
    • So the 7 are this machine's environment, not this change. The causes the assertions name: macOS tmpdir() resolving to /private/var/… against an expected /var/…, and no util-linux script(1) for os dev on a real terminal. The 7 are the two published-subpath-* pins, dev-boot-output-order, published-entry-node-env-source-reroute, serve-runtime-state-project-key, verify-dashboard-windows and plan.reads-env-files. CI runs all of them.
  • ESLint on the three changed TypeScript files, with --no-inline-config: exit 0. node scripts/check-doc-authoring.mjs: clean. node scripts/check-cross-package-test-inputs.mjs: OK.
  • origin/main has moved two commits past the base (6422da58e8, 7a6ba03109). Neither touches packages/cli or packages/spec/src/migrations.

Acceptance notes


Generated by Claude Code

…-ownership plan stops refusing v17 databases

sys_scim_provider was retired before the first ADR-0131 D10 census, so
neither census counted it and the inventory carried no row. Its ADR-0087
entry (scim-provider-object-retired) leaves the table in place untouched,
by ruling, so every database that booted an earlier release still holds it
with organization_id, and the plan threw uninventoried-platform-table.

Add a fate-4 (report) row under a new census population,
retired-before-census, citing the ADR-0087 entry, with notNullExempt: the
object is retired, no declaration anchors tenancy on the column, and a
connection scoped to no organization was a legitimate row.

Pins: the inventory test freezes the population, checks it is uncounted
and unregistered, that the cited ADR-0087 entry exists and still leaves
the table untouched, and that the citation carries no tracker number. The
plan integration test plans a v17-shaped sys_scim_provider under both
postures as report, with the inventory-less control refusing it by name.

Claude-Session: https://claude.ai/code/session_local_f4c0ec33-a0e1-425c-a1fd-59bcb3256ad8
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 16, 2026
@github-actions

github-actions Bot commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/releases/v17/17-3.mdx (via sys_scim_provider (literal, a string literal in object))

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
  • 4 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 — 29 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 98ba1bf1d28d44bddd18b3dcff5db2502e8ad6a9 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1973be44c8fe35a72f9593a64c8ab6cc3c9cef5e — the merge of head 6137d10b22aa5cd68f99d7d8d33af59edc8445dc into base 98ba1bf1d28d44bddd18b3dcff5db2502e8ad6a9, 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 1973be44c8fe35a72f9593a64c8ab6cc3c9cef5e && git checkout 1973be44c8fe35a72f9593a64c8ab6cc3c9cef5e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 98ba1bf1d28d44bddd18b3dcff5db2502e8ad6a9 6137d10b22aa5cd68f99d7d8d33af59edc8445dc && git checkout -B drift-repro 98ba1bf1d28d44bddd18b3dcff5db2502e8ad6a9 && git merge --no-ff 6137d10b22aa5cd68f99d7d8d33af59edc8445dc

node scripts/docs-audit/affected-docs.mjs --json 98ba1bf1d28d44bddd18b3dcff5db2502e8ad6a9

⚠️ 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 98ba1bf1d28d44bddd18b3dcff5db2502e8ad6a9 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

main brought the legacy shipped-set seeds category into the same inventory
file; the only conflict was the test file's import block, resolved by
keeping both imports (MIGRATIONS_BY_MAJOR and securityDefaultPermissionSets).

Claude-Session: https://claude.ai/code/session_local_f4c0ec33-a0e1-425c-a1fd-59bcb3256ad8
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit bef7bdc Oct 16, 2026
40 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/sharp-matsumoto-1ec135 branch October 16, 2026 14: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 Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant