Skip to content

feat(driver-sql,driver-turso)!: 18.x step 7 part 7b-0 — a table the driver creates carries its tenant column NOT NULL - #24940

Merged
objectstack-fleet[bot] merged 84 commits into
mainfrom
claude/issue-24583-7b0-tenant-not-null-ddl
Oct 21, 2026
Merged

objectstack-fleet[bot] merged 84 commits into
mainfrom
claude/issue-24583-7b0-tenant-not-null-ddl

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Part of #24583 (18.x step 7, part 7b-0; claim 6285142472; governing text: execution record 6285122667, ADR-0131 D1, ruling ② A 6269955919).

Clause-②: yes (narrowing) — a table the driver creates refuses a write that names no organization, at the database.
Metadata-protocol: none — the constraint changes what a data writer can store, not what a metadata author can write.

Organization delete: resolved on main (#24948, ruling B)

Earlier on this branch, deleting an organization answered 500. The engine's set_null cascade on the injected tenant lookup hit the NOT NULL column. The same defect is live in 18.0.0 on a ceremonied database (measured; see #24948).

The fix landed on main as #25016 (688052e), and this branch now carries it. On this head:

  • A delete refuses before any write while the organization owns records: 409 DELETE_RESTRICTED with per-table dependents.
  • The delete door removes the organization's own auth structure (members, invitations, teams, team members and org-admin grants) as one unit.
  • The engine refuses where it used to set_null a tenant column. Pointer columns (active_organization_id, parent_organization_id) keep set_null.

The two dogfood pins that were red here are green on that contract, with no 500. #25016 rewrote them:

  • organization-delete-federated-fixture: 6/6. An organization that owns nothing is deleted with 200 (the positive control). An organization that owns a record is refused with 409 and its counts. The Default Organization cases are kept.
  • org-record-admin-reach: 7/7.

What changes

  • driver-sql createColumn: a new tenantColumn argument makes the column NOT NULL on both exits (scalar and multi-value). Only the two CREATE TABLE sites pass it:

    • initObjects' create branch: name === tenantField;
    • ensureShardTable's create branch: asked of the base's declaration through the recording resolver. The index sync now reuses that same answer.

    An ADD COLUMN never passes it, so a column added to a populated table stays nullable. That table is the ceremony's.

  • driver-turso remote face: buildCreateTableSQL (used by syncSchema, syncSchemasBatch and initObjects) asks the driver's computeTenantField resolver. It emits NOT NULL on the tenant column only. ADD COLUMN is unchanged.

  • spec: TENANT_SCOPE_FIELD_DEF keeps its shape. Its description no longer says "NULL on single-tenant stacks", and its docblock says why the column has no storage.notNull.

  • No metadata change and no differ change. ceremonyOwnedNullability (from feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) #24681) already keeps the declared-nullable / physical-NOT-NULL difference out of drift. Pinned for a fresh table, a new shard and the remote face.

Pins

  • driver-sql/src/sql-driver-tenant-column-not-null.test.ts, on SQLite and live PostgreSQL 16 (MySQL cell declared un-run):
    • a fresh table refuses a raw NULL tenant insert at the database, and reports no drift;
    • a newly rotated shard does the same, with no drift on the shard;
    • boundaries: an opted-out object keeps organization_id nullable; a tenant column added to an existing table stays nullable; a pre-existing shard keeps its nullability.
  • driver-turso/src/turso-remote-tenant-column-not-null.test.ts: a real libSQL file: client behind the remote face (no hosted remote reached).
    • Through all three schema doors, the remote CREATE matches the local face's notnull flags, refuses a raw NULL tenant insert, and reports no drift on either face.
    • Boundaries: an opted-out object, and an ADD COLUMN on an existing remote table.
  • Ablation, by reverting the || tenantColumn term: driver-sql 2 cases go red, Turso remote 4 cases go red; restored, green.

Fixtures (no assertion weakened)

About 200 test files across 25 packages wrote organization-less rows through a fresh table. Each was fixed one of three ways, chosen by what the test is about:

  1. The write names its organization, where the organization was incidental (for example a decorated driver that dropped options, or a raw insert).
  2. A lean engine gets what a real single install has: installDefaultOrganization (a managedBy: 'better-auth' sys_organization and its row, seeded at rest). The engine's D9 derivation then stamps the writes. In cli, a fixture plugin is spliced in ahead of the app's seed.
  3. A pre-18 store, where organization-less rows ARE the subject: NULL-bucket uniqueness, the driver's NULL arm, global grants and positions, legacy seeds, backfills. The table is relaxed through the driver's own relax_not_null drift door (pre-18-tenant-column.testkit.ts). The Turso remote face has no such door, so there the testkit creates its table with the transport's own pre-18 CREATE. These are the readers 7a-1/7a-2/7a-3/7b-2 retire. Their tests keep measuring the shape they were written for.

Size, for the approval

3352 changed lines (+3271 / -81) in 367 files, against main edcc8ce:

  • driver and spec code: 55 lines (sql-driver.ts 30, remote-transport.ts 16, injected-system-column-provenance.ts 9);
  • the two new pins: 319 lines;
  • testkits: 1235 lines in 27 files, mostly the same two small kits copied into each package that needs them (default-organization.testkit.ts, pre-18-tenant-column.testkit.ts), because a package's tests do not import another package's test files;
  • fixture re-stamps in existing tests: 1631 lines;
  • the changeset: 20 lines.

A database the published 18.0.0 created

  • ORGANIZATION_OWNERSHIP_SHAPE_GENERATION is unchanged (1). NOT NULL on new tables is not a shape the guard certifies.
  • The harness's fixture-18.0.0 leg (upgrade-18x-harness.dogfood.test.ts, a real 18.0.0-born database: nullable tenant columns, birth marker generation 1) is green on this head, 4/4. HEAD plans no destructive change over it and boots it with no refusal. ceremonyOwnedNullability keeps its nullable tenant columns out of drift.
  • Such a database does not have to stay nullable. On a copy of that fixture, HEAD's os migrate organization-ownership (plan, then apply, with auth composed) answered complete: true, blocking: 0 and tightened 62 of its 65 tenant tables to NOT NULL. The 3 it left nullable:
    • a rotated activity shard (the ceremony reports it cannot tighten a shard);
    • sys_email_template (mirror-deletion fate);
    • sys_sso_provider (tenancy disabled, with an authored organization_id).
      This is a measurement only; nothing here changes the ceremony.

Merge resolutions (main 7169612, then 688052e)

Measured (merged tree: main 7169612, with #24681, 7a-1, 7a-2, 7a-3, 7b-3, 7c, 7z and #24911)

  • On the latest merge (head 0b58ee3, main 688052e with fix(objectql, plugin-auth)!: organization delete refuses while records remain, removes its own auth structure as one unit, and never orphans a tenant column #25016):

    • driver-sql 3707 passed; driver-turso 2437; service-messaging 602; objectql 8566; plugin-auth 3376;
    • dogfood: the upgrade harness (including the fixture-18.0.0 leg) and every organization and org-* dogfood file, 19 files and 131 tests, none skipped;
    • workspace turbo run typecheck: 151/151;
    • dispatch-gates --commands: 103 commands, all exit 0;
    • check-tenant-chokepoint: exit 0.
  • On the merge before it (main 7169612):

    • upgrade harness 12/12, and the merge-touched dogfood files 71/71;
    • driver-sql 3696 passed; driver-turso 2437; plugin-webhooks 227;
    • workspace turbo run typecheck: 151/151;
    • check-tenant-chokepoint: exit 0.
  • Every package's suite is green except the two organization-delete dogfood cases above. cli: 6046 passed. dispatch-gates --commands: 103 commands, all exit 0. Typecheck of the 23 touched packages: clean. pnpm lint: exit 0.

  • Main's own new fixtures were also brought to the NOT NULL column. Among them is 7b-3's "a lean embedding keeps __global__" pin: it now runs on a pre-18 store, because a fresh table refuses the organization-less row at the database. That is a cross-part question for the seat; see the report.

  • R1: fresh pnpm dev -- --fresh on random ports, for showcase, crm and todo. Each answered health 200 and seeded-admin sign-in 200, with 0 ERROR lines and 0 NOT NULL refusals. The fresh database's tenant columns are all NOT NULL.

  • R2 + R3: upgrade-18x-harness.dogfood (8, over the ceremonied fixture) and organization-ownership-boot-refusal-order.dogfood (4) are green.

  • R4: a production os serve under OS_TENANCY_POSTURE=isolated, with the real @objectstack/organizations linked into a scratch app, answered health 200, owner sign-up 200 and sign-in 200, with 0 error lines. Step 4's seven walled e2e cases (serve-organizations-host-resolution 3 + serve-organizations-mount-failure 4, run with OS_TEST_TIERS=nightly) pass 7/7.

  • Known NULL writer, made loud: a production os serve with no OS_AUTH_SECRET (showcase) still answers health 200, and its inline seed is refused 172 times at the database. Before, it landed NULL rows. Part 7b-3 makes that boot refuse.

  • Lean SQL embedding (@objectstack/objectql/core + SqlDriver, no organization object): an author sees NOT NULL constraint failed: account.organization_id on the first insert. ADR-0131 D9 prescribes an engine refusal that names what is missing. Today it is the database's.

Changeset

.changeset/24583-tenant-column-not-null-ddl.md: patch for driver-sql, driver-turso and spec (Metadata-protocol: none, under main's level rule from #25114). It keeps the BREAKING banner, the ADR-0087 disposition and the remedy. Since review 6317931247 (B4), it also says that on a database that has not taken the ceremony, a table this release creates is NOT NULL beside nullable siblings.

⚠️ Changed lines exceed 3,000 (3352 by check-governed-merges --branch; no governed paths; see Size above), so this lands the Tier H way: an authorized approval, or a human merge.

🤖 Generated with Claude Code

https://claude.ai/code/session_018EszgtA3V2KXBX5tf59Hi4

claude added 30 commits October 19, 2026 13:06
…r retired; webhook activation door

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…he 18.0 dual read; door pins for webhook

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…asserts no row and a dispatch

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…the checklist, the webhooks guide and the showcase; changeset

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…tcher's switch read

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…posture builds no wall, as its 18.0 seeded copy stamped with the Default Organization was

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…red writes; the declared-webhook double is drivable by the limit gate

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…does not implement

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…authors; checklist and admitsOrganization docblock say a declared webhook is deployment-wide under single

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…the fixture's customized declared-webhook mirror

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…n fail on a double delivery

Claude-Session: https://claude.ai/code/session_01PDSxBZE561TFe1N2iEjKXZ
Co-authored-by: Claude <noreply@anthropic.com>
…0g-webhooks-registry

# Conflicts:
#	content/docs/permissions/tenant-audit-census.mdx
#	docs/audits/2026-08-tenant-audit-write-call-sites.counts.md
…0g-webhooks-registry

# Conflicts:
#	content/docs/permissions/tenant-audit-census.mdx
#	docs/audits/2026-08-tenant-audit-write-call-sites.counts.md
…-registry' into claude/issue-24581-ceremony-open
…he ceremony, then boot; boot-refusal-order step 2 back on the binary
…el — the cipher names no organization

Claude-Session: https://claude.ai/code/session_014UATXWqAXqkaZkbG92ox6V
Co-authored-by: Claude <noreply@anthropic.com>
…sys_flow_credential leave reach)

Claude-Session: https://claude.ai/code/session_014UATXWqAXqkaZkbG92ox6V
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4f41fbf1a4c6f8818e4e9945b2772e983a28bb20
Local-runs: none
Scope: packages/drivers/driver-sql/src/sql-driver.ts, packages/drivers/driver-turso/src/remote-transport.ts, packages/spec/src/data/injected-system-column-provenance.ts, .changeset/24583-tenant-column-not-null-ddl.md, 27 new testkit files (default-organization.testkit.ts in 17 packages and examples; pre-18-tenant-column.testkit.ts in driver-sql, driver-turso, runtime, service-storage, plugin-approvals, plugin-auth, plugin-security, dogfood; cli default-organization-plugin.testkit.ts and pre-18-store.testkit.ts), plus 331 test files; read to judge them: packages/drivers/driver-sql/src/schema-drift.ts, packages/drivers/driver-turso/src/turso-driver.ts, packages/objectql/src/engine.ts, packages/objectql/src/tenancy/system-write-organization.ts, packages/objectql/src/lifecycle/lifecycle-service.ts, packages/spec/src/system/migration.zod.ts, packages/platform-objects/src/identity/sys-api-key.object.ts, sys-session.object.ts, sys-organization.object.ts, packages/plugins/plugin-security/src/bootstrap-platform-admin.ts, auto-org-admin-grant.ts, packages/plugins/plugin-security/src/tests/legacy-catalog-objects.testkit.ts, packages/runtime/src/app-plugin.ts, packages/verify/src/read-coercion.ts, date-bucket-parity.ts, packages/qa/dogfood/test/upgrade-18x-harness.dogfood.test.ts, .github/workflows/upgrade-harness-optin.yml, .github/workflows/ci.yml, AGENTS.md (branch and origin/main), docs/adr/0131-total-organization-ownership-no-null-organization-id.md.

Read, in a detached worktree at the head and read-only: the PR's own diff against its merge-base with origin/main (f46ef4d; 362 files, +3259 / -75), every one of the 75 deleted lines, the two new pins, every testkit, the per-package justification beside every pre-18 relax call (asPre18TenantColumn, initObjectsPre18, relaxTenantColumns, relaxEveryTenantColumn, relaxEveryRegisteredTenantColumn, relaxTenantColumnsOnFile), and 24 changed test files across driver-sql, driver-turso, objectql, runtime, rest, cli, plugin-security, plugin-approvals, plugin-auth, plugin-audit, plugin-sharing, plugin-webhooks, service-messaging, service-storage, service-automation, service-analytics, dogfood and app-showcase (cli orphans.integration.test.ts and rest rest-stored-expression-dropped-fields.test.ts among them). Context read from the GitHub API: the PR body, issue #24583 (the 7b-0 part), execution record 6285122667, claim 6285142472, rulings 6266750393, 6267391172, 6269955919 and 6270035914 on #15194, the dev report 6311401309 and the later spec-seat note 6317289149, the seat note 6309760075, #24948 and #25016 (merged as 688052e, on this branch), the check-runs and annotations on the head, and the Test Core (2/6) job log tail. Nothing was built, run or posted.

① Derived judgments

1. Scope of NOT NULL — confined to CREATE TABLE, tenant column only. PASS.

  • driver-sql: createColumn takes a new fifth argument tenantColumn: boolean = false (sql-driver.ts:19743). Both exits add the constraint only when the declaration says so OR the argument is true: the multi-value exit at :19794 and the scalar exit at :20216. Exactly two call sites pass it: initObjects' create branch, name === tenantField (:12742), where tenantField is this.tenantFieldByTable[tableName] recorded by computeAndRecordTenantField (:12668); and ensureShardTable's create branch, name === baseTenantField (:12155), asked of the base's declaration through the recording resolver (:12137) and reused by the shard's index sync (:12198). The two ADD COLUMN sites (:12164 inside the shard's alterTable, :12782 inside the managed table's) call createColumn without the argument, so an added column stays nullable. No other createTable site builds a managed table: :7934 is the sequences table (no tenant column) and :14725 is the SQLite rebuild, which copies each physical column's existing nullability (c.nullable, :14730), so a fresh table's NOT NULL survives an unrelated rebuild and an old table's nullable column is not tightened by one.
  • driver-turso remote face: buildCreateTableSQL asks tenantFieldResolver once per CREATE (remote-transport.ts:2439) and passes name === tenantField into columnDeclarationSQL (:2446), whose new argument defaults to false (:2503-2506). The two ADD COLUMN emitters (:2124 in syncSchema, :2255 in syncSchemasBatch) call it with one argument. The resolver is the driver's own computeTenantField (turso-driver.ts:2050), the same predicate the local face uses; the local face is SqlDriver itself (class TursoDriver extends SqlDriver, turso-driver.ts:1746), so both faces answer alike, which the remote pin holds equal per door (turso-remote-tenant-column-not-null.test.ts:105-118).
  • Other create paths: MySQL and PostgreSQL go through the same Knex createColumn; the pin runs on every provisioned dialect cell (live PostgreSQL 16; the MySQL cell is declared and not run in CI). Schema-qualified tables change nothing about the column clause. Shard rotation is covered above and pinned for a rotation without a tenancy block (sticky opt-out preserved by computeAndRecordTenantField, :12280-12292). A renamed tenant field (tenancy.tenantField) is what computeTenantField returns (:12259-12264), so only that column is constrained and a sibling organization_id stays nullable, matching the engine's resolveTenantFieldName precedence (system-write-organization.ts:199-213). An opted-out object (tenancy.enabled: false) returns null (:12257), so name === null never matches; pinned on both drivers.
  • A non-tenant column cannot receive NOT NULL by mistake: the predicate is equality with the one resolved name; a multi-value tenant field would only add NOT NULL to a JSON column, harmless.

2. 18.0.0-born and ceremonied databases — untouched. PASS.

  • No metadata change beyond the description text, no differ change: schema-drift.ts is not in the diff. The pre-existing ceremonyOwnedNullability rule (schema-drift.ts:940-965, threaded as this.resolveTenantField(tableName) at sql-driver.ts:13964) reads a NOT NULL tenant column under a nullable declaration as no drift, so a fresh HEAD table, a new shard and a remote table report none (pinned); an 18.0.0-born table (nullable column, nullable declaration) reports none as before, so no relax_not_null or tighten_not_null is proposed on either kind of database.
  • Nothing on HEAD assumes NOT NULL at the database. The engine's organization-delete contract from fix(objectql, plugin-auth)!: organization delete refuses while records remain, removes its own auth structure as one unit, and never orphans a tenant column #25016 refuses by rule, not by constraint: isTenantColumnSetNull (engine.ts:20290-20301) and refuseOrganizationDeleteWhileOwned (:20443) run regardless of the physical column. The D9 derivation (resolveSystemInsertOrganization, :7658-7710, applied to every insert at :16635) is unchanged.
  • ORGANIZATION_OWNERSHIP_SHAPE_GENERATION = 1 (migration.zod.ts:215); no file in the diff touches it.
  • R2 and the fixture-18.0.0 leg on the head: the 18.x upgrade harness (opt-in) check is green on 4f41fbf; its job runs exactly test/upgrade-18x-harness.dogfood.test.ts (upgrade-harness-optin.yml:118-119), which holds the ceremonied-fixture R2 and R3 cases (:137-279) and the fixture-18.0.0 leg (:297-352: birth marker generation 1, "HEAD's schema plan over it holds no destructive change", boots with no refusal, every row kept).
  • One residual edge, not measured here and not a defect of this PR's contract: a table HEAD creates on an unceremonied 18.0.0-born deployment (a new rotation shard, a cold table a newly configured lifecycle tier creates through syncSchema) is NOT NULL while its siblings are nullable, so a pre-existing NULL-organization row moved into it by the lifecycle mover (cold.upsert, lifecycle-service.ts:1502) would be refused. Those rows are the ceremony's (D10), which is the ADR's answer. See B4.

3. Writers. PASS, with two declared loud paths (B2, B3).

  • Every production write reaches the driver through ObjectQL.insert, whose D9 step derives the organization for any write that carries none (engine.ts:16635 → :7658): already-carried on the context or every row → nothing to do; the object is federated or has no tenant field → nothing to do; else derived under single with the organization object registered, or refused with SystemWriteOrganizationRequiredError. The only arm that lets an organization-less row through is no-organization-object (:7703), a composition that registers no sys_organization.
  • Production writers checked: plugin-security's grants go through ql.insert with the system context (bootstrap-platform-admin.ts:374, the grant owned by the Default Organization per D5 at :12, :465-492, :561-575; auto-org-admin-grant.ts:235; invitation-placement.ts:202; suggested-audience-bindings.ts:807). No production code defines or writes sys_permission_set: it is a retired catalog object that exists only as test scaffolding (__tests__/legacy-catalog-objects.testkit.ts:3-13, "No runtime code declares them any more"). The grep for direct driver-level writes outside the drivers finds only the engine's own dispatch (engine.ts:8452, :8492, :16876, :16879), the verify probes (read-coercion.ts:74, date-bucket-parity.ts:191: probe objects whose fields carry no organization_id, so no tenant column) and the lifecycle mover (lifecycle-service.ts:1502, rows copied with the organization they already carry). No production source writes organization_id: null (the only hits are comments and the organizations plugin's claim of orphans, claim-orphan-org-rows.ts:94, which reads NULL to replace it). Boot seeds, webhooks (declared webhooks read from the registry since feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) #24681, no seeder), approvals, messaging, audit (sys_audit_log has systemFields.tenant: false), sys_job_run, os db orphans --delete (the engine's own delete) and the REST/data API all write through the engine. Pointer columns active_organization_id (sys-session.object.ts:227; sys-api-key.object.ts:322 with tenancy: { enabled: false } at :76) and parent_organization_id (sys-organization.object.ts:414) are ordinary declared lookups, so createColumn builds them nullable.
  • The two compositions that can still present a NULL to a fresh table are the ones the changeset declares: a production os serve whose AuthPlugin did not compose (no OS_AUTH_SECRET), and a lean embedding (@objectstack/objectql/core or createStandaloneStack over a SQL driver with no organization object). In both the engine's no-organization-object arm passes the row and the database refuses it. The dev measured the first (health 200, 172 seed rows refused, 0 ERROR lines; seed rejections are counted per row, app-plugin.ts:1979, not thrown). Before this PR each landed an orphan row, which ADR-0131 D9 already names a refused write. Non-blocking: B2, B3.
  • The pre-18 relax calls (the seat's addendum), every use on the branch judged by its subject:
    • driver-sql, 15 files via asPre18TenantColumn: the NULL-bucket unique key (sql-driver-unique-tenancy, legacy-tenant-composite-null-distinct, 8577-tenant-scoped-declared-unique, tenant-scoped-declared-unique, sys-setting-organization-unique, sys-position-organization-unique, declared-index-organization-respelling), the driver's NULL arm and read doors (tenant-scope, tenant-scope-read-doors, adr0120-three-posture-conformance, upsert-cross-organization-refusal), the organization-less counter bucket (organizationless-counter-refusal, 7b-3's pin, now explicitly a pre-18 store; the cross-part note the dev raised stands), and one unstamped-row case in 21227-create-answers-stored-row whose other cases are re-stamped with organization_id: ORG. Each is a deliberate model of rows only a pre-18 column can hold, the readers 7a-1/7a-2/7b-2 retire.
    • driver-turso, 4 files via initObjectsPre18: the remote face has no relax door, so the kit issues the transport's own pre-18 CREATE (pre-18-tenant-column.testkit.ts:52-58). inherited-members keeps the schema-sync-stats cases 18.x-shaped (pre18Tenant = false) with the assertion { created: 2, existing: 0 } unchanged; drift-detection keeps all 8 cases (8 before, 8 after) and marks one pre-18; migration-entries (duplicates case) and local-remote-tenant-scope-parity (the chokepoint ledger's NULL arm). Deliberate.
    • runtime, 3 files: 17.x residue replayed by the seed loader (seed-loader-null-organization-holder), the autonumber split under a NULL bucket (seed-tenancy-autonumber-split), and default-organization-statements.golden (two uses: the CONTROL case's organization-less grant, deliberate; and the delete-and-recreate case, which relaxes every tenant table with a comment citing a cascade set_null the engine no longer performs since fix(objectql, plugin-auth)!: organization delete refuses while records remain, removes its own auth structure as one unit, and never orphans a tenant column #25016; the test itself expects the 409 at :217, so the relax dodges nothing, but the comment is stale: B5).
    • service-storage, 3 files: a backfill over legacy organization-less sys_file rows, the TTL sweep reaching legacy sys_upload_session rows, the tenant-audit half repairs. Deliberate (sweeps and backfills over legacy rows are their subject).
    • plugin-sharing, 4 files with a local copy of the helper: organization-less sys_record_share, sys_sharing_rule and sys_share_link rows as fixtures for the stamp and reconcile pins; the production stamp itself is asserted unchanged (the headers at record-share-organization-stamp.test.ts:1-14 and share-link-organization-stamp.test.ts:1-13 say the service stamps or refuses). Deliberate.
    • plugin-security: 6 narrow uses on sys_permission_set (controlled-by-parent-every-master-edit, -master-object-read, -write-shape-master-edit, insert-seam-judges-organization, readable-ids-nested-compose, readable-ids-delegated-compose), each the retired legacy catalogue row a pre-18 store holds, with the rig's other driver writes re-stamped (readable-ids-nested-compose.test.ts:246-266 adds organization_id: ORG); and about 30 blanket relaxEveryRegisteredTenantColumn / relaxEveryTenantColumn calls in rigs whose subject is the legacy catalogue, global sets, positions and holdings. plugin-auth (10 blanket) and plugin-approvals (2 blanket) follow the same pattern. Each is a legitimate pre-18 model; the blanket form relaxes tables with no residue too, which lowers that rig's power to catch an unstamped production write (B6, non-blocking).
    • cli, 3 files via relaxTenantColumnsOnFile, and dogfood, 2 files: the ceremony's input is a pre-18 store (organization-ownership.no-organization, .declared-rule-mirrors, platform-global-column-drop; sharing-rule-org-less-caller, email-template-boot-sweep). Deliberate.
    • No use relaxes a column to let the code under test land a NULL that production would land; where the write was incidental the fixture names its organization or installs the Default Organization instead.
  • Fixture changes: all 75 deleted lines read. They are re-stamped raw writes, SYSTEM constants replaced by organization-carrying contexts, two helper signatures widened (seeded(face, file, pre18Tenant), the 4-tuple CASES), the old description string in one pin, the two spec lines and the five driver lines. No .skip, .only, .todo, xit or xdescribe was added; no assertion was removed or loosened in any of the 24 files read. cli orphans.integration.test.ts:355-359: the raw od_hold row gains ...ORG, the restrict assertion unchanged. rest rest-stored-expression-dropped-fields.test.ts:70-72: installDefaultOrganization(engine) before the inserts, assertions unchanged.

4. Organization delete under NOT NULL. PASS. With #25016 on the branch: a set_null lookup that is the child's tenant column into sys_organization is never cleared; it is counted and the delete refused with 409 DELETE_RESTRICTED before any write (engine.ts:19907, :19919, :20290-20301, :20443-20459). A predicate delete of sys_organization is refused before any hook (:20792, organizationPredicateDeleteRefusal :754). A user delete and a team delete clear lookups into sys_user and sys_team, never a tenant column (isTenantColumnSetNull requires the deleted object to be sys_organization and the field to be the child's resolved tenant field). Pointer columns keep set_null (keepsOrganizationReference, :20348-20351) and are nullable by construction (item 3). Rows that outlive the organization (append-only, engine-owned) keep their reference, never NULL (:20325-20328).

5. Walled postures (R4) and single (R1). CI runs no dogfood shard on a pull request (ci.yml:2037, github.event_name != 'pull_request'), so the Dogfood Regression Gate success on the head is the aggregator's vacuous answer. The evidence on the head is the dev's measurement in the PR body: R1 as fresh pnpm dev -- --fresh boots of showcase, crm and todo (health 200, seeded-admin sign-in 200, 0 ERROR lines, 0 NOT NULL refusals, every tenant column NOT NULL); R4 as a production os serve under OS_TENANCY_POSTURE=isolated with @objectstack/organizations linked (health 200, owner sign-up and sign-in 200) and step 4's seven walled e2e cases 7/7 under OS_TEST_TIERS=nightly. The cli suite, which serves artifacts through createStandaloneStack with the Default Organization spliced ahead of the app seed, is green in the shard (162 files, 2180 tests). R2 and R3 are CI-verified on the head (item 2). B7 asks the seat's delta check to re-ask R1 and R4 at the final head.

6. Spec text. The only spec change is injected-system-column-provenance.ts: the description Tenant scope (stamped on insert by org-scoping; never NULL — a write that names no organization is refused). (:120-121) and a docblock (:105-110) saying why the shape keeps required: false and no storage. Accurate as the protocol's statement (D1, ruling ② A: the differ reads the column's nullability as the ceremony's or the driver's, never as drift, so a storage.notNull would make the differ propose tighten_not_null on every 18.0.0-born table and contradict ruling ② A; its absence is justified). The pin registry-tenant-index-declaration.test.ts follows the new text. Two stale comments remain outside the shape: packages/spec/src/system/constants/system-names.ts:225 ("it stays NULL on single-tenant stacks") and packages/objectql/src/registry.ts:602 (B8, text only).

② Semver level

  • .changeset/24583-tenant-column-not-null-ddl.md: @objectstack/driver-sql: minor, @objectstack/driver-turso: minor, @objectstack/spec: patch; Clause-②: yes (narrowing); the adr-0087 marker present, disposition not-required (no-migration-prescription) with its reason; a BREAKING banner; FROM ("Before"), TO ("Now", "Unchanged") and the Fix ("The remedy") stated. Under the rule in force at this head's merge-base (branch AGENTS.md:1087-1088: yes takes at least minor, (narrowing) is BREAKING) minor is the right level, and Check Changeset is green on the head.
  • Every sentence read as true against the code: the three CREATE sites, ADD COLUMN unchanged, the shard and the remote face reported by the ceremony as unable to tighten (feat(cli,runtime)!: the upgrade ceremony opens — v17 and 18.0 databases take it, and a server serves only a database it created or one the ceremony certified (18.x steps 5 and 6) #24681), no drift either way (item 2), the opted-out object untouched, the upsert refusal (the database checks the proposed row before conflict resolution on all three dialects), the two compositions that now fail loudly, and the remedy's three forms. One sentence will go stale at the next main merge: "shipped as minor under the repo's launch-window convention for breaking changes" (see next bullet).
  • After the next merge of main the branch meets feat(gates): the changeset level follows the metadata protocol — Metadata-protocol: line, level axis, console refresh at patch, release notes by reader action (ruling A) #25114 (main at f2b6cf2, AGENTS.md:1083-1096 on origin/main): a second line-initial declaration Metadata-protocol: added | narrowed | semantic | none is read in the PR body and the changeset body, and the level follows the kind alone (none takes patch; a non-protocol breaking change ships patch with its BREAKING banner and ADR-0087 disposition kept). Neither the PR body nor the changeset carries the line today, so Check Changeset will read not-measured-material on its next run after the merge. The spec seat's lead (6317289149) and this review's reading agree on none: the DDL constraint changes what a data writer can write, not what a metadata author can write, and the spec change is description text. That means driver-sql and driver-turso regrade to patch and the "launch-window" sentence is rewritten. Not a FAIL at this head; B1.

③ Boundary flags

  • B1 (not blocking). The changeset and PR body will need Metadata-protocol: none after the next main merge (feat(gates): the changeset level follows the metadata protocol — Metadata-protocol: line, level axis, console refresh at patch, release notes by reader action (ruling A) #25114), with driver-sql and driver-turso regraded from minor to patch and the launch-window sentence rewritten; carriers (BREAKING banner, adr-0087 marker, remedy) kept. The declaration is the dev's; the seat's delta check should see it on the final head.
  • B2 (not blocking). A production os serve whose AuthPlugin did not compose (no OS_AUTH_SECRET) registers no organization object, so the engine's no-organization-object arm (engine.ts:7703) passes its inline seed rows and a fresh table refuses them at the database; the boot serves (health 200) and the seed loader counts the rows as rejected (app-plugin.ts:1979). Before, those rows were orphans, which D9 already forbids. The outcome is the ADR's, delivered one layer low; the PR names 7b-3 as the part that makes that boot refuse. Declared in the changeset's "Who sees it".
  • B3 (not blocking). A lean embedding with no organization object (@objectstack/objectql/core or createStandaloneStack over a SQL driver) sees the database's NOT NULL constraint failed on the first write to a tenant-scoped object instead of an engine refusal that names what is missing (D9's prescription). Declared BREAKING with the remedy (carry organization_id or context.tenantId, compose auth, or declare systemFields: { tenant: false } / tenancy: { enabled: false }).
  • B4 (not blocking). On an unceremonied 18.0.0-born deployment a table HEAD creates (a new rotation shard, a cold table a newly configured lifecycle tier creates) is NOT NULL beside nullable siblings; a pre-existing NULL-organization row moved into it by the lifecycle mover (lifecycle-service.ts:1502) would be refused. Not measured; those rows are D10's, and the harness's fixture-18.0.0 leg (which holds a rotated shard) is green. Worth one sentence in the release notes or the ceremony's docs.
  • B5 (not blocking, text). packages/runtime/src/default-organization-statements.golden.test.ts:200-209 relaxes every tenant table with a justification citing the engine's set_null cascade on organization_id, which fix(objectql, plugin-auth)!: organization delete refuses while records remain, removes its own auth structure as one unit, and never orphans a tenant column #25016 retired on this branch; the test expects the 409 at :217, so the relax dodges nothing. The comment should say the pre-18 store holds organization-less grants, or the blanket relax should go.
  • B6 (not blocking). About 42 rigs in plugin-security, plugin-auth and plugin-approvals use the blanket relaxEveryRegisteredTenantColumn / relaxEveryTenantColumn where a named list of the residue-holding tables (sys_permission_set, sys_position, sys_user_permission_set, holdings) would do. Each is a legitimate pre-18 model; the blanket form also relaxes tables with no residue, so an unstamped production write in such a rig lands instead of failing. A follow-up could narrow them; no assertion is weakened by it.
  • B7 (not blocking). R1 and R4 at this head rest on the dev's measurement, since CI runs no dogfood shard on a pull request (ci.yml:2037). R2 and R3 are CI-verified by the opt-in harness. The seat's delta check should re-ask R1 and R4 at the final head, as the plan already says.
  • B8 (not blocking, text). Two comments still say the tenant column stays NULL on single-tenant stacks: packages/spec/src/system/constants/system-names.ts:225 and packages/objectql/src/registry.ts:602.
  • CI on the head (item 8). Red: Test Core (2/6) and its parent Test Core; the shard's annotation names packages/plugins/plugin-security pnpm run test exited 1 (cli in the same shard passed 162 files / 2180 tests; completeness guard OK, 17152 tests accounted for). Known red, being fixed by the dev; not a FAIL on its own. Pending: none. Skipped by design on a pull request: Dogfood Regression Gate (n/3), Dogfood Verify CLI, Build Docs, Console Pin Gate, Temporal Conformance, Packed-tarball smoke. Green and load-bearing: 18.x upgrade harness (opt-in) (R2, R3, fixture-18.0.0 leg), Check Changeset (branch rule), Check PR Size, Governed Surface Queue Guard, Spec property liveness, all Lint and Type Check jobs, Test Core (1/6), (3/6), (4/6), (5/6), (6/6), Build Core.

Implemented-by: claude/issue-24583-7b0-tenant-not-null-ddl
Reviewed-by: session_01Y5583bs2RDd53MH6kM1ca3

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 · 请批准 · epic 席 session_01Y5583bs2RDd53MH6kM1ca3(os-steve)· 2026-10-21T13:59Z

需要你做的:在本 PR 点 Approve(Review changes → Approve)。 本 PR 改动约 3,260 行,超过 3,000 行,属于 Tier H,要你批准后才能合入。合并门禁只认人工批准记录,不要求批准落在最新提交上,所以之后再合 main 也不会让批准失效。批准后,等 CI 全绿、epic 席做完最后一版的差异复核,就设为可审查并开启自动合并,你不用再做别的。

  • 改了什么: 18.x 第 7 步的 7b-0。新建一张表时,组织列(organization_id)在数据库层就是 NOT NULL,没有组织的写入会被数据库直接拒绝:
    • 只在建表时生效(driver-sql 的建表和分片建表,driver-turso 远程建表),而且只作用于组织列;
    • 往已有表上加列时仍然可空。老库由升级仪式负责,本 PR 不碰;
    • 不改元数据,不改差异检测,ORGANIZATION_OWNERSHIP_SHAPE_GENERATION 保持 1。
  • 为什么改: 这是 ADR-0131 D1:组织归属在数据库层也必须完整。
  • 规模为什么大: 约 3,260 行里,生产代码只改了 driver-sql、driver-turso 和 spec 里的一段说明,其余都是测试数据,要给写入的行补上所属组织。
  • 验证:
  • 合并前还要做的(不影响你批准):
  • 对用户的影响: 嵌入式用法里,没有组织对象的写入,原来留下无主的行,现在会直接报 NOT NULL constraint failed。changeset 里写明了补救办法:带上组织,或者关闭租户字段。
  • 风险与回滚: 只影响新建的表。不含数据迁移;回滚即还原本 PR。
  • 随下一个版本发布。

Generated by Claude Code

…very-row-master fixtures meet a NOT NULL tenant column

Their organization-less permission-set (and, for the update seam, position) catalogue rows are legacy residue, so each suite relaxes only those tables to the pre-18 shape. No assertion changed.

Claude-Session: https://claude.ai/code/session_018EszgtA3V2KXBX5tf59Hi4
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat note · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-21T14:58Z.

  • Approved. os-zhuang (GOVERNED_APPROVERS) approved this PR at 2026-10-21 14:57Z, at 4f41fbf1a, after the Tier H request 6317949842.
  • The maintainer's instruction, verbatim: 「你后面修复了就可以自己入队了」. The seat readies the PR and arms auto-merge once the final head meets three conditions:
    • it carries the Test Core (2/6) plugin-security fixtures, the main merge, and B1 (Metadata-protocol:), B4, B5 and B8 of 6317931247;
    • CI is green on it;
    • the seat's delta check against 6317931247 passes, re-asking R1 and R4 at that head.
  • The approval is not pinned to a head (Governed Surface Queue Guard), so later pushes keep it.

…its BREAKING banner; stale NULL-tenant comments corrected

- Changeset: `Metadata-protocol: none` (the constraint changes what a data
  writer can store, not what a metadata author can write), so driver-sql and
  driver-turso regrade from minor to patch under main's level rule. The
  launch-window sentence goes; the BREAKING banner, the ADR-0087 marker and
  the remedy stay. It now also says that, on a database that has not taken
  the ceremony, a table this release creates is NOT NULL beside nullable
  siblings, so the lifecycle mover cannot carry an organization-less row
  into it.
- runtime default-organization-statements.golden: the delete-and-recreate
  case no longer relaxes every tenant table. The engine's cascade no longer
  sets a tenant column to NULL, and the case passes on the fresh store.
- spec system-names and objectql registry docblocks: the tenant column is
  stamped on insert and never NULL, not "NULL on single-tenant stacks".

Claude-Session: https://claude.ai/code/session_018EszgtA3V2KXBX5tf59Hi4
Co-authored-by: Claude <noreply@anthropic.com>
@os-zhuang
os-zhuang marked this pull request as ready for review October 21, 2026 15:20
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review (delta check after PASS at 4f41fbf)

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a674501c47d27d92cc2e673630b01f67bea44b15
Local-runs: none
Scope: packages/plugins/plugin-security/src/controlled-by-parent-every-row-master.test.ts, packages/plugins/plugin-security/src/delegated-organization-membership.test.ts, packages/plugins/plugin-security/src/update-seam-judges-organization.test.ts, .changeset/24583-tenant-column-not-null-ddl.md, packages/objectql/src/registry.ts, packages/runtime/src/default-organization-statements.golden.test.ts, packages/spec/src/system/constants/system-names.ts; read to judge them: AGENTS.md (head, 1083-1100), scripts/check-changeset-no-major.mjs, scripts/pm/clause2-line.mjs, scripts/check-adr-0087-registration.mjs (the #25114 delta), .github/workflows/pr-automation.yml, packages/plugins/plugin-security/src/pre-18-tenant-column.testkit.ts, packages/plugins/plugin-security/src/tests/legacy-catalog-objects.testkit.ts, packages/objectql/src/engine.ts (main's diff f46ef4d..edcc8ce and the head), packages/plugins/plugin-security/src/security-plugin.ts, packages/plugins/cloud-connection/src/cloud-connection-plugin.ts and packages/drivers/driver-sql/src/sql-driver.ts (main's diff), packages/objectql/src/lifecycle/lifecycle-service.ts, packages/spec/src/data/object.zod.ts, packages/objectql/src/registry-tenant-index-declaration.test.ts, the PR body.

Read, in a detached worktree at the head and read-only: the four commits since 4f41fbf (first-parent: cceb73d, 31189e7, 7fe7c4e, a674501), the two own commits' full diff (201 lines, seven files), the two merges replayed with git merge-tree --write-tree and compared tree-for-tree against the recorded merge trees (cceb73d: dde01868ee replayed equals dde01868ee recorded; 7fe7c4e: e5603fbdda equals e5603fbdda), plus git show --remerge-diff on each (header only, no resolution hunk), the three main fixtures diffed against origin/main at edcc8ce, the changeset and the gate rule at the head, main's 20 first-parent commits between f46ef4d and edcc8ce with the production (non-test) source they changed (31 files), the PR via the REST API (head a674501, draft, mergeable_state blocked), the check-runs and annotations on the head and on 31189e7, and the main ruleset's required contexts. Nothing was built, run or posted. The worktree was removed afterwards.

① Derived judgments

1. The plugin-security fixtures for the three tests from main. PASS.

  • The three files differ from origin/main (edcc8ce) by exactly 15 added lines and 0 removed: one import and one commented relaxTenantColumns call each. No assertion is weakened, skipped or removed; no .skip, .only, .todo, xit or xdescribe anywhere in the three files.
  • controlled-by-parent-every-row-master.test.ts: relaxTenantColumns(engine, ['sys_permission_set']) at :171 under the comment :168-170 ("exists only on a store built before 18.x ... that table is this store's"). The one organization-less row is the driver-level sys_permission_set create at :202-205 (organization_id: null, the retired legacy catalogue row); every other write carries ORG (:181 sys_member, :207 grants, :210 masters, :303 details through getDriver(detail).create with organization_id: ORG).
  • delegated-organization-membership.test.ts: relax at :141, comment :138-140; the organization-less row is :152-155 (sys_permission_set, organization_id: null); sys_member :149, grants :159 and seeded rows :163 all carry their organization.
  • update-seam-judges-organization.test.ts: relax at :152 over ['sys_permission_set', 'sys_position'], comment :149-151 naming both; the rows are :166-169 (two permission sets, organization_id: null) and :171 (sys_position pos_clerk written through the driver with no organization at all; SysPosition in legacy-catalog-objects.testkit.ts:26 declares no tenancy opt-out, so its injected column is NOT NULL on a fresh store and the relax is needed). Grants :174, :177, position holding :178 and seeded rows :181 carry OWN_ORG or the loop's organization.
  • Each is the narrow, named-table form of the kit (pre-18-tenant-column.testkit.ts:15-30, through the driver's own relax_not_null drift apply door), the same shape as the six narrow sys_permission_set uses the full review approved; none is the blanket form (B6 does not grow). The engine entry it calls exists (engine.ts:22374 getDriverForObject). This is the fix for the Test Core (2/6) red at 4f41fbf: CI runs on the merge ref, so main's three new suites met the NOT NULL column there before they were on the branch.

2. B1 under the new changeset gate (#25114). CLOSED.

  • The rule at the head, AGENTS.md:1086-1093: two line-initial declarations in the PR body and the changeset body, Clause-②: yes|no with at most one arm and Metadata-protocol: added|narrowed|semantic|none, "the change to what a metadata author can write"; "The level follows the kind alone: a kind takes at least minor on a package the diff moves, none takes patch; a non-protocol change, widening or breaking, ships patch and keeps its BREAKING banner and ADR-0087 disposition."
  • The gate mechanizing it: scripts/check-changeset-no-major.mjs:909-910 (none takes every introduced entry patch), :2049-2053 (the three none verdicts: none-baseline-moved, none-minor, none-clean), :2041-2046 (the two contradictions, neither reachable from none), :2023-2030 (materiality: both lines must be readable). The reader, scripts/pm/clause2-line.mjs:527 and :558-575, takes the first token after the colon and accepts trailing reasoning, so the PR body's line 3 Clause-②: yes (narrowing) — ... reads yes/narrowing and line 4 Metadata-protocol: none — the constraint changes what a data writer can store, not what a metadata author can write. reads none. pr-automation.yml:1149 runs the gate against the merge base and reads both lines from the event payload (:4-5).
  • none is the correct reading. The change is a storage constraint at CREATE TABLE in driver-sql and driver-turso and a description docblock in spec; no authorable key, value or metadata type is added, removed or tombstoned, no requirement on an author is tightened (the injected field keeps required: false and no storage, full review item 6), and no authorable key's meaning or default changes, so it is neither added, narrowed nor semantic. The spec seat's lead (6317289149) read it the same way.
  • patch is the level the gate requires: with none, any minor is refused (none-minor, :2051), as it was on 31189e7 (run 119302114668: "metadata-protocol declares none while a changeset entry is graded minor or above ... Under none the level is patch"). The PR introduces one changeset (git diff edcc8ceae6..head --name-status -- .changeset: one A), all three entries patch (changeset :2-4), and moves nothing under packages/spec/authorable-surface/ or authorable-defaults/, so the verdict is none-clean.
  • Carriers kept: the ! title :7, Clause-②: yes (narrowing) :9, Metadata-protocol: none :10, the single adr-0087 marker :12 (not-required (no-migration-prescription), which the registration gate on the head reports as "ADR-0087 exemption (no-migration-prescription)"), the BREAKING banner :14, FROM/TO :16-18, "Who sees it" :19 and the remedy :21. Under feat(gates): the changeset level follows the metadata protocol — Metadata-protocol: line, level axis, console refresh at patch, release notes by reader action (ruling A) #25114 the (narrowing) arm stays the breaking signal and none adds no signal, so the disposition is still read and still required; it is present.
  • Every sentence true: :14 now says it "ships as patch with this banner" and the launch-window sentence is gone; :16-17, :19 and :21 are unchanged from the full review's reading; :18 gains the B4 sentence (next item), and its other claims (ADD COLUMN stays nullable, shard and remote face reported by the ceremony, no drift either way, opted-out objects untouched) are unchanged.
  • Check Changeset on the head: completed success (run 119304051905), with the ADR-0087 exemption notice as its only annotation and no failure annotation. The two failures on 31189e7 (not-measured-material, then none-minor after the PR body was edited) are the ones the full review predicted and the head's regrade resolves.

3. B4, B5, B8.

  • B4 CLOSED. The sentence is at changeset :18: "On a database that has not taken the ceremony, a table this release creates (a newly rotated shard, or a cold table a newly configured lifecycle tier creates) is NOT NULL beside nullable siblings: an organization-less row the lifecycle mover would carry into it is refused there, and stays where it is until the ceremony attributes it." Accurate: the two creators are the ones the full review named (ensureShardTable, and syncSchema on a cold datasource), and the mover's order makes "stays where it is" true, lifecycle-service.ts:1501-1504: cold.upsert(object, row, ['id']) per row, then hot.bulkDelete only after every upsert; the file's own rule at :1498-1500 says a throwing upsert means bulkDelete never runs and the hot rows survive for the next sweep. The PR body (line 112) states the same.
  • B5 CLOSED. a674501 deletes the eleven-line blanket loop and its stale cascade comment from default-organization-statements.golden.test.ts (old :198-208). At the head the delete-and-recreate case (:199-243) runs on the fresh, NOT NULL store: it still expects the 409 DELETE_RESTRICTED control (:206-209), moves every owned row to a holding organization (:218-229), deletes and recreates (:230-231), re-points (:234-236) and asserts the memo's retirement (:240-242). Removing the relax makes the case stronger, not weaker: had the engine's organization delete written a NULL into any tenant column, the delete at :230 would now fail at the database, so the case also pins fix(objectql, plugin-auth)!: organization delete refuses while records remain, removes its own auth structure as one unit, and never orphans a tenant column #25016's contract on a fresh store. The one relax left in the file is the CONTROL case's narrow sys_user_permission_set (:176-179), whose subject is an organization-less grant, with its own comment; the import at :40 stays used. The case is still meaningful.
  • B8 CLOSED as named. system-names.ts:223-226 now reads "org-scoping stamps it on insert (under single with the Default Organization), and it is never NULL (ADR-0131 D1)"; registry.ts:600-604 says the same with "Required-false in the declaration". Both are text only. Three further lines with the old wording, not named by B8, remain: C1 below.
  • B6 stays open by design; the delta adds three narrow relaxes and removes one blanket one, so the blanket count falls by one.

4. Main's merges and new writers. PASS: no new NULL writer.

② Semver level

.changeset/24583-tenant-column-not-null-ddl.md: @objectstack/driver-sql: patch, @objectstack/driver-turso: patch, @objectstack/spec: patch, under Clause-②: yes (narrowing) and Metadata-protocol: none. Correct under ruling A as written at AGENTS.md:1091-1093 and mechanized at check-changeset-no-major.mjs:2049-2053: a non-protocol breaking narrowing ships patch with its BREAKING banner and ADR-0087 disposition, both present. Check Changeset is green on the head.

③ Boundary flags

  • C1 (not blocking, text). Three further comments still carry the pre-18 wording B8 named: packages/objectql/src/registry.ts:777 ("they can always stamp the column, it just stays NULL when no tenant context exists"), packages/spec/src/data/object.zod.ts:2499-2501 ("It stays NULL on single-tenant stacks and is auto-stamped on insert by @objectstack/organizations ... in multi-tenant mode", a docblock) and packages/objectql/src/registry-tenant-index-declaration.test.ts:151-152 (a comment beside an assertion that only checks the column is defined). Comment text only; no code or assertion depends on it. A follow-up or a rider on the next text PR.
  • B7 (not blocking, carried). R1 and R4 were not re-measured here (Local-runs: none). The delta's own commits change no production behaviour (a changeset, two docblocks, three test fixtures and one test relax removed), so the dev's measurement at 4f41fbf transfers to the branch's own code; main's merged code is what Test Core now exercises on the head.
  • CI on the head (read 15:17:56 UTC; the runs were created 15:14:38 UTC, five minutes after the push). Red: none. Green: Check Changeset, Check PR Size, Governed Surface Queue Guard (a required context), Spec property liveness, Dogfood Regression Gate (the aggregator, vacuous on a pull request), Auto Label, Check Documentation Links, the claim guards, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests. In progress: Test Core (1/6) through (6/6), Build Core, Type Check · workspace, · consumer gates, · source gates, · debt ledger, six Lint lanes, 18.x upgrade harness (opt-in) (R2, R3), The card this PR closes must claim this branch. Not yet reported: the aggregators Test Core, TypeScript Type Check and Lint & Repo Gates. The main ruleset's required contexts are TypeScript Type Check, Test Core, Build Core, Lint & Repo Gates, Governed Surface Queue Guard; one of the five is green, four are pending. Test Core (1/6 to 6/6 and the aggregator) must be green before the seat lands; it is still running. Skipped by design on a pull request: Build Docs, Console Pin Gate, Dogfood Regression Gate (n/3), Dogfood Verify CLI, Packed-tarball smoke (opt-in), Temporal Conformance. On 31189e7 the TypeScript Type Check red was the aggregator reading its two lanes as cancelled (superseded by the next push), not a type error. The PR is still marked draft (draft: true, mergeable_state: blocked), which the seat will need lifted before enqueueing.

Implemented-by: claude/issue-24583-7b0-tenant-not-null-ddl
Reviewed-by: session_01Y5583bs2RDd53MH6kM1ca3

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat ACCEPT · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-21T15:53Z. PR #24940 at a674501c47d27d92cc2e673630b01f67bea44b15 (#24583 part 7b-0; claim 6285142472; os-dev-report 6311401309). ACCEPT.

Seat check

  • Shape.
    • 367 paths, +3,271 / -81 on the final head. Production code is driver-sql, driver-turso and one spec description. The rest is test fixtures that give each written row its organization.
    • check-governed-merges: NOT governed, over 3,000 lines, so Tier H.
  • Tier H approval.
    • Request 6317949842: the Chinese 速读, a review request to os-zhuang, needs-user-decision, and a push.
    • os-zhuang APPROVED at 2026-10-21 14:57Z.
    • Maintainer, verbatim: 「你后面修复了就可以自己入队了」 (seat note 6319162669).
    • The approval is not pinned to a head.
  • Review. CONTRACT_REVIEW_TIER, narrowing face:
  • Line rules on the final head:
    • R2 and R3: the 18.x upgrade harness (opt-in) check is green, fixture-18.0.0 leg included.
    • R1 (dev-measured on a674501c4): showcase, crm and todo each boot fresh with health and admin sign-in 200, and no NOT NULL refusal.
    • R4 (dev-measured on a674501c4): a walled os serve with the organizations runtime answers health, owner sign-up and sign-in 200. 32 of 33 tenant tables are NOT NULL; the nullable one is sys_sso_provider, which opts out. The walled e2e cases pass 7/7.
    • ORGANIZATION_OWNERSHIP_SHAPE_GENERATION stays 1.
  • Gate check-runs on the head:
    • The required checks (TypeScript Type Check, Test Core, Build Core, Lint & Repo Gates, Governed Surface Queue Guard) all succeeded.
    • All 43 latest check-runs are success or skipped.
  • Out of scope, and where it goes:
    • B6 (narrowing the blanket relax rigs) and C1 (three more stale "stays NULL" comments) go to one follow-up card.
    • B2 and B3 (a no-auth or lean boot sees the database's NOT NULL refusal) are declared in the changeset. 7b-3 owns the boot refusal.
  • Landing: the seat arms auto-merge. If the merge queue removes the PR, the seat diagnoses it, delta-checks the fix and re-arms.

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 21, 2026
Merged via the queue into main with commit 5e9b100 Oct 21, 2026
43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-24583-7b0-tenant-not-null-ddl branch October 21, 2026 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants