Skip to content

feat(cli): os migrate plan lists the tenant columns still nullable, naming the voluntary ownership ceremony; the ceremony reports a NOT NULL shard truthfully - #25179

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-25134-migrate-plan-nullable-advisory
Oct 22, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-25134-migrate-plan-nullable-advisory

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #25134

Clause-②: yes (widening) — os migrate plan --json gains the nullableTenantColumns field, and the ceremony's not-null step gains the already-not-null outcome and the alreadyNotNull summary count. No exit code, drift entry, op, refusal or accepted input moves.
Metadata-protocol: none

Executes the maintainer's ruling 6328195425 (letter A, items 2 and 4) under the claim 6333411407. Dispatched by the epic seat (epic:#15194, session_01Y5583bs2RDd53MH6kM1ca3). Branch from origin/main ab488d9b7d (carries #25133's fix, #25155).

The claim expected Clause-②: no. I re-declared it from the diff as yes (widening): two machine-read outputs gain a field and an outcome value. Nothing is narrowed, and the level stays patch (Metadata-protocol: none).

What changed

1. os migrate plan: one advisory section (ruling item 2). New module packages/cli/src/utils/nullable-tenant-columns.ts, wired into packages/cli/src/commands/migrate/plan.ts.

  • What it lists: every tenant column the database still holds nullable, by table and column, with os migrate organization-ownership as the remedy. The section prints after the unmanaged-table section and before the drift verdict, so it shows on an "in sync" plan too. An 18.0.0-born store has no drift, so this matters.
  • The population is exactly the column the differ already exempts as the ceremony's (ceremonyOwnedNullability). It is read from the same source: each planned driver's tenant field per table (tenantFieldByTable), plus one catalog query per database for nullability. A rotation shard is read through its base.
    • An object with tenancy disabled has no tenant field, so sys_sso_provider is never listed.
    • A table no object declares (the retired sys_notification_subscription) is not listed either; the unmanaged-table section already reports it.
  • Advisory only:
    • It is not drift. It adds no entry to changes, nothing to total, and proposes no op.
    • It changes no exit code.
    • Under --json it is the separate field nullableTenantColumns: { status: 'read', adr: 'ADR-0131 D1', remedy, columns: [{ table, column, database?, ceremony: 'tightens' | 'leaves', reason? }] }, or { status: 'unreadable', adr, detail }.
    • It is always present once a SQL driver is found, so columns: [] (read, none) cannot be confused with not read.
    • ceremonyOwnedNullability is untouched (no driver file in the diff).

Residue: marked apart, the dispatch's open question. I read 6323585273 §1 and marked residue, because that is both true and simple. Without the mark, a ceremonied store would be told to run the ceremony again for columns it never tightens. Each column carries ceremony: 'tightens' | 'leaves', and a leaves column carries its reason. The reasons come from facts the plan already holds:

  • a rotation shard (rotationBaseOf): created before shards were created NOT NULL, and dropped when its period leaves the base's retention;
  • the driver's driftApplyLimit('tighten_not_null') (the Turso remote face);
  • the ceremony inventory: notNullExempt, or the mirror-deletion fate (sys_email_template).

When no listed column is one the ceremony tightens, the footer says so and does not offer the ceremony. One case is deliberately not told apart: a table still holding a NULL row the ceremony cannot attribute. That would need a row count per table, and the ceremony's own plan already reports it.

2. The stale shard sentences (ruling item 4). In packages/cli/src/utils/organization-ownership-not-null.ts, the docblock (:27-31) and the shard detail (:46) said each new shard is created nullable. That has not been true since 7b-0, and both are corrected.

  • A shard the driver created NOT NULL is now reported already-not-null: nothing to do, no note, counted in the step's new alreadyNotNull summary key. This comes from the new optional NotNullTarget.isNotNull, which os migrate organization-ownership wires to the same catalog read.
  • A shard created nullable before 7b-0 stays unsupported, and its note now says it leaves with the base's retention.

3. Docs.

  • content/docs/deployment/cli.mdx: a new "Tenant columns still nullable (advisory)" subsection in the os migrate plan section, with the human output (as printed on the fixture) and the --json shape.
  • content/docs/upgrading.mdx: one paragraph in "Upgrading to 18.0".
  • Not touched: content/docs/releases/**, docs/adr/**, the shape generation, and any boot path.

Measured on fixture-18.0.0 (HEAD's os migrate plan, with the served environment's OS_AUTH_SECRET)

  • Raw 18.0.0 store: 65 tables carry a nullable organization_id. The advisory lists 63 of them. The 2 not listed are sys_sso_provider (opt-out) and sys_notification_subscription (retired, undeclared).
    • 61 are tightens, which matches the ceremony's measured applied 61.
    • 2 are leaves: sys_activity__r20261020 (the pre-7b-0 shard) and sys_email_template.
  • After the voluntary ceremony: only that residue is listed, all leaves.
    • The ceremony's not-null step reports HEAD's own shard as alreadyNotNull ≥ 1, never unsupported.
  • A fresh HEAD store: columns: []. Its only nullable organization_id is sys_sso_provider.

Pins

pin where
the advisory on fixture-18.0.0: lists every nullable tenant column and only those; residue marked; no drift entry packages/qa/dogfood/test/upgrade-18x-harness.dogfood.test.ts, the 18.0.0 leg
after the ceremony: lists only the residue; the ceremony reports HEAD's shard alreadyNotNull same leg
a fresh store lists nothing same leg; utils/nullable-tenant-columns.test.ts; plan.nullable-tenant-columns.integration.test.ts
--json shape plan.nullable-tenant-columns.integration.test.ts; plan.apply-limit.test.ts (key set)
exit code unchanged (both faces) plan.nullable-tenant-columns.integration.test.ts
no drift op added (changes/total byte-equal to the fresh store's) plan.nullable-tenant-columns.integration.test.ts; utils/nullable-tenant-columns.test.ts (the driver's own drift scan)
the shard report utils/organization-ownership-not-null.test.ts (real SQLite shard, both arms); organization-ownership-apply.generation-1.test.ts (step summary)

The harness helpers runSchemaPlan and runOwnershipCeremony gain an optional env. The new legs pass SERVED_ENV (an OS_AUTH_SECRET), as a production os serve of the showcase must have. The existing legs are unchanged.

Proof (head 8ad60d9a)

  • Changed CLI files, locally: 4 files / 30 tests pass. That includes the spawned plan.nullable-tenant-columns.integration.test.ts, which this diff adds to the integration tier.
  • cli unit tier: vitest run --project unit reported 321/322 files and 4754/4756 tests on the first run. The 2 red were plan.apply-limit.test.ts pinning the --json key set, which this PR deliberately widens. The lists were updated, and the file plus plan.journal-mode-note.test.ts re-ran at 19/19. The rest of the integration tier is declared to CI (Test Core).
  • cli typecheck: exit 0 (tsc --noEmit and check:test-typecheck).
  • dogfood typecheck: exit 0. The first run was aborted by the 4 GB heap; I re-ran it with --max-old-space-size=6144.
  • eslint on the 10 changed .ts files: exit 0.
  • Upgrade harness, whole file (upgrade-18x-harness.dogfood.test.ts, both the 18.0 and the 18.0.0 legs): 15/15. Lock VERDICT command-exit 0.
  • Ablations. Each mutation went through scripts/ablation-replace.mjs; the tool verified each landed on disk and was restored to blob == HEAD.
    • A1, the nullability filter removed: 5 red.
    • A2, residue marking removed: 3 red.
    • A3, the advisory counted into total: red at the total assertion.
    • A4, process.exitCode = 1 when the section prints: red at the human exit assertion.
    • A5, the --json field dropped: red at the fresh-store field.
    • A6, the shard isNotNull check removed: red at the already-not-null pin.
    • My first attempts at A1, A4 and A5 were refused by the tool (anchor or replacement counts that could not move) and were re-run with moving anchors. The refused attempts measured nothing.
    • The CLI pins run from source, so no rebuild was needed. The harness pins were not ablated (they run dist).
  • node scripts/pm/dispatch-gates.mjs --commands: 100 families, each exit code captured before any pipe. --ran reconciliation: 100 accounted, 99 run, 1 NOT MEASURED. All 99 exit 0, after two re-runs:
    • check-plugin-teardown-shape --self-test first exited 3 on the shallow clone; it passed after a fetch of its pinned commit.
    • check:skill-examples first exited 3 because client-react was unbuilt; it passed after building it.
    • NOT MEASURED: check:dual-build-cjs-loads exits 3 without a whole-monorepo build. It is declared to CI.
    • After the last commit, I re-ran 10 families on head 8ad60d9a (check:type-check-coverage, check:type-check-debt, check:cross-package-test-inputs, check:test-source-alias, check:tier-file-adoption, check:durability-log-level, both changeset gates, check:nul-bytes, check:doc-authoring). All exit 0.
  • check-governed-merges --branch origin/claude/issue-25134-migrate-plan-nullable-advisory: exit 0, NOT governed (0 of 15 paths), +926 / −33.
  • R1:
    • pnpm dev:crm -- --fresh reached ready, health 200.
    • A production os serve of examples/app-crm with OS_AUTH_SECRET on a scratch database reached ready, health 200.
    • os migrate plan on that fresh store: nullableTenantColumns: { status: 'read', columns: [] }, nothing printed, "in sync", exit 0.
  • R4:
    • A walled production os serve (OS_TENANCY_POSTURE=group, OS_AUTH_MEMBERSHIP_POLICY=invite-only, OS_PLATFORM_OWNER_EMAIL, a secret) of a scratch app declaring @objectstack/organizations reached ready with Organizations composed, health 200.
    • os migrate plan under the same environment: columns: [], exit 0. The only nullable organization_id is sys_sso_provider.

Acceptance notes

  • The advisory depends on the plan's own environment. os migrate plan run without OS_AUTH_SECRET composes no auth family, as os serve does. The advisory then cannot see the auth family's tenant tables (sys_member, sys_team, sys_user_permission_set, …), and the unmanaged-table section lists them as declared by nothing. Measured on the fixture: 52 listed without a secret, 63 with one. This is the existing composition rule for the whole plan, not the advisory's. The docs already say to run a migration with the deployment's environment.
  • origin/main moved 6 commits since the branch point (72a302a9). None touches this diff's files or behaviour, and merge-tree is clean. Not merged, per the post-PR merge rule; CI tests the merge ref.
  • Edits were first attempted on the session's primary checkout. The worktree guard refused them, and all work was done in a linked worktree on the same branch.

Generated by Claude Code

@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Oct 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

11 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/concepts/metadata-lifecycle.mdx (via os migrate organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.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 organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.ts), 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/deployment/tenancy-modes.mdx (via os migrate organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.ts))
  • content/docs/kernel/services-checklist.mdx (via os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/permissions/capabilities.mdx (via os migrate organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.ts))
  • content/docs/permissions/permission-sets.mdx (via os migrate organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.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 organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))

⛔ 8 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 organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.ts), os migrate plan (command, read off packages/cli/src/commands/migrate/plan.ts))
  • content/docs/releases/v18/index.mdx (via os migrate organization-ownership (command, read off packages/cli/src/commands/migrate/organization-ownership.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
  • 12 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 72a302a9dde3347e15a971c5da28df205496a217 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 535453a9c8c580a5aba191e2d5142bd6d2cb5b8d — the merge of head 8ad60d9a9518ca6a65d48c4ce94397e40512cf97 into base 72a302a9dde3347e15a971c5da28df205496a217, 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 535453a9c8c580a5aba191e2d5142bd6d2cb5b8d && git checkout 535453a9c8c580a5aba191e2d5142bd6d2cb5b8d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 72a302a9dde3347e15a971c5da28df205496a217 8ad60d9a9518ca6a65d48c4ce94397e40512cf97 && git checkout -B drift-repro 72a302a9dde3347e15a971c5da28df205496a217 && git merge --no-ff 8ad60d9a9518ca6a65d48c4ce94397e40512cf97

node scripts/docs-audit/affected-docs.mjs --json 72a302a9dde3347e15a971c5da28df205496a217

⚠️ 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 72a302a9dde3347e15a971c5da28df205496a217 → 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

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 8ad60d9a9518ca6a65d48c4ce94397e40512cf97
Local-runs: none
Scope: the whole diff (15 files, +926 / -33, merge-base ab488d9b7d). Read to judge it: packages/cli/src/utils/organization-ownership-plan.ts, packages/cli/src/utils/organization-ownership-inventory.ts, packages/cli/src/utils/unmanaged-tables.ts, packages/cli/src/utils/schema-migration-plugins.ts, packages/drivers/driver-sql/src/sql-driver.ts, packages/drivers/driver-turso/src/turso-driver.ts, packages/cli/src/index.ts, packages/cli/package.json, .github/workflows/upgrade-harness-optin.yml.

The face is the declaration face: check-governed-merges reads 0 governed paths, nothing under packages/spec/src/**, and the PR body and changeset declare Clause-②: yes (widening), not a narrowing. So the lane seat records it at TIER_DEFAULT. Inputs: the ruling 6328195425, the claim 6333411407, the dev report 6334476182, the PR body, the diff, and the head's check-runs. Nothing was built or run locally.

① Derived judgments

Ruling item 2: the advisory lives in os migrate plan. Executed as ruled.

  • The section is computed at packages/cli/src/commands/migrate/plan.ts:283, before the "in sync" return. An 18.0.0-born store has no drift and every such column, so the advisory has to come first.
  • It is printed as its own block at :433, above the drift verdict.
  • Under --json it is a separate key at :334. It is outside total and changes.
  • No exit path moves: the only this.exit calls in the file are the pre-existing failure paths (:234, :236, :517, :519), and the diff adds none.
  • No boot line: the diff touches no serve or boot file.
  • No drift op: the differ's ceremonyOwnedNullability exemption is untouched, and the harness pins that no nullability_mismatch names organization_id.

Population.

  • The tenant field per table comes from each planned driver's tenantFieldByTable, the same map resolveTenantField reads.
  • sys_sso_provider (tenancy disabled) and undeclared tables are not listed. The harness pins that the only nullable organization_id tables left out on fixture-18.0.0 are exactly those two, sys_notification_subscription and sys_sso_provider, and that nothing is listed that is not nullable.
  • A shard is read through rotationBaseOf (nullable-tenant-columns.ts:219).
  • "Could not read" never reads as "none nullable":
    • a plan that did not compose the served object set is unreadable (:202);
    • so is an unreadable driver map, or a failed or missing catalog read;
    • columns: [] is reserved for a read that found none.
  • Identifiers are inlined only after the SAFE_IDENTIFIER filter (:113).
  • The Postgres and MySQL forms use the same ANY (current_schemas(false)) and DATABASE() idiom as unmanaged-tables.ts:206 and organization-ownership-plan.ts:616. Only SQLite was measured; PG and MySQL are read from code.

Residue marking against what the ceremony really does. Each leaves reason in ceremonyLeaves (:167) is the same condition the ceremony applies:

  • Shard. A nullable shard is unsupported in applyNotNullThroughDrift (organization-ownership-not-null.ts:51).
  • Transport. It is the same driftApplyLimit('tighten_not_null') call (:172 against organization-ownership-not-null.ts:71). The Turso remote face answers needs-table-rebuild for that op (turso-driver.ts:684).
  • notNullExempt. The plan sets willReceive: false with that same reason (organization-ownership-plan.ts:1123).
  • mirror-deletion. willReceive: false (:1098).
  • column-drop tables. These are not in the population, because their objects carry no tenant field (inventory citations systemFields.tenant: false). This is judged from the citations plus the measured fixture: 61 tightens against the ceremony's measured applied 61. It was not checked table by table.

Ruling item 4: the stale shard sentences and the shard outcome.

  • The old sentence ("each new shard is created … nullable") is gone. The driver claim that replaces it holds: ensureShardTable creates the base's tenant column NOT NULL (sql-driver.ts:12225, name === baseTenantField).
  • isNotNull is asked of a shard only, on the shard's own database driver (organization-ownership.ts:888). An unreadable answer is null and falls back to unsupported, which is the conservative side.
  • already-not-null is counted in alreadyNotNull (organization-ownership-apply.ts:3728, :3747). It adds no note and no blocker, and the step stays done. This is pinned by the new 5d case and by the harness leg (alreadyNotNull at least 1, and no "created … nullable" note).

The --json widening.

  • The two key-set pins in plan.apply-limit.test.ts widen by exactly the one key, and every other assertion is unchanged.
  • NotNullOutcome and the advisory types are not reachable from the CLI's exports map (., ./console and ./hook-body; src/index.ts names neither). The widened union is CLI report output, not a published type.

Tests.

  • No pre-existing assertion was weakened.
  • The harness gains three legs on the 18.0.0 describe: plan before, after the voluntary ceremony, and a fresh store.
  • SERVED_ENV is passed only by the new legs. runOwnershipCeremony and runSchemaPlan take an optional env with default {}, so the existing callers are unchanged.
  • The dev's six ablations each turn a named pin red, judged structurally from the pins.

Prose (cli.mdx, upgrading.mdx, changeset), sentence by sentence against the diff.

  • The section's text, remedy and placement match renderNullableTenantColumns. The example's indents match it too: the first line through printInfo, the bullets at 6 + 2 spaces, the last line at 6.
  • "Turso remote: every listed column is one the ceremony leaves" matches the driftApplyLimit branch.
  • "A fresh install … prints nothing" is pinned by the fresh-store leg.
  • The JSON shapes match the types, except that the docs' unreadable shape omits its adr key. That is minor.
  • One imprecision, not blocking: for an attribution or report table that still holds a NULL row the ceremony cannot attribute, the advisory says tightens, but the ceremony's plan says willReceive: false. The module doc states this limit (nullable-tenant-columns.ts, header). The section's last line and upgrading.mdx ("tightens them to NOT NULL when you run it") say it without the condition. The remedy line sends the operator to the ceremony's own --out PLAN, which names such a table, so nothing is hidden.

② Semver level

@objectstack/cli: patch, Metadata-protocol: none. Correct under the changeset gate: there is no protocol face, and the change is a widening (one added --json key, one added outcome value and summary count). No accepted input, refusal or exit code moves. Check Changeset is green on the head.

③ Boundary flags

  • Clause-② re-declared yes (widening) against the claim's expected no. Accepted, because the declaration is accurate. It is what puts this review on the declaration face at TIER_DEFAULT, and both the PR body and the changeset carry it.
  • open_questions: none.
  • The out-of-scope finding (a plan run without OS_AUTH_SECRET composes no auth family, so the advisory sees 52 of 63 on fixture-18.0.0). This is the whole plan's existing composition rule, not this PR's. The plan already says so in that case and names the remedy (schema-migration-plugins.ts:2051-2057 on main: re-run with the secret). Nothing to file.
  • R2. This PR changes the 18.x upgrade harness, and that lane is opt-in. It was skipped at open, so needs:upgrade-harness was applied, and the lane is running on this head. Landing waits on it, outside this verdict.
  • Nit, not blocking: in packages/qa/dogfood/test/upgrade-harness/ceremony.ts, the new NullableTenantColumn interface was inserted between runSchemaPlan's doc block and the function, so that doc now sits detached above the interface.

Implemented-by: session_01RXxXFcq659RGJE6SsXAc3u
Reviewed-by: session_01Y5583bs2RDd53MH6kM1ca3

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat ACCEPT · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-22T07:07Z. PR #25179 at 8ad60d9a9518ca6a65d48c4ce94397e40512cf97 (#25134; ruling 6328195425, letter A; claim 6333411407; os-dev-report 6334476182). ACCEPT.

Seat check

  • Shape: the cli os migrate plan advisory (utils/nullable-tenant-columns.ts), the ceremony's not-null shard outcome, tests, the upgrade harness legs, cli.mdx, upgrading.mdx, and one changeset. 15 files, +926 / -33. check-governed-merges --pr 25179: 0 of 15 paths governed, so this is an ordinary queue landing.
  • What it does:
    • os migrate plan lists every tenant column still nullable and names os migrate organization-ownership as the remedy. Each column says whether the ceremony tightens it or leaves it, and why.
    • The section is advisory only. It is not drift, proposes no op, does not count in total, and moves no exit code. Under --json it is the separate nullableTenantColumns field.
    • The ceremony now reports a rotation shard the driver created NOT NULL as already-not-null instead of unsupported, and the stale shard sentences are corrected.
    • No boot line, no shape-generation change, and no docs/adr/** or releases/** edit.
  • Review: TIER_DEFAULT, declaration face. The dev re-declared Clause-②: yes (widening), which is accurate: one --json key and one outcome value are added. PASS 6334904919 at this head.
  • R2: this PR changes the 18.x upgrade harness, whose lane is opt-in. The seat applied needs:upgrade-harness. The lane ran on this head (job 119687975985) and passed 15/15, including the fixture-18.0.0 legs and the three new ones: plan before the ceremony, after it, and on a fresh store.
  • Measured (dev report): fixture-18.0.0 lists 63 columns: 61 tightens, which equals the ceremony's measured applied 61, and 2 leaves (a pre-7b-0 shard and sys_email_template). After the ceremony, only those 2 are listed. A fresh store lists none. R1 and R4 boots are ready, with health 200.
  • CI: 47 check runs on the head, none failing. The required checks (TypeScript Type Check, Test Core, Build Core, Lint & Repo Gates, Governed Surface Queue Guard) succeeded, and Check Changeset is green. The skipped runs are opt-in lanes.
  • Not blocking, not filed:
    • The advisory's tightens (and upgrading.mdx) does not carry the condition that a table still holding unattributable NULL rows stays nullable after the ceremony. The remedy line sends the operator to the ceremony's own --out PLAN, which names such a table.
    • A detached doc block in the harness helper ceremony.ts.
    • The no-secret composition gap is the plan's existing rule, and the plan already names its remedy.
  • Landing: the seat flips the PR ready and enables auto-merge. Fixes #25134 closes the card. The merge commit is then handed to docs(adr): ADR-0131 18.x notes, review precision (B1–B3, B6) and the #25155 / #25179 citations (Tier H) #25154 for its last citation (B4 part 2, D1).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 22, 2026 07:08
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 22, 2026 07:08
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 22, 2026
Merged via the queue into main with commit 6251f59 Oct 22, 2026
48 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-25134-migrate-plan-nullable-advisory branch October 22, 2026 07:29
os-steve pushed a commit that referenced this pull request Oct 22, 2026
…migrate plan advisory

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UQCyrGmMy6kwav9XCzAuQ9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants