Skip to content

feat(driver-sql,driver-turso,driver-memory,objectql,cli,metadata-protocol): plain organization key part in new unique indexes; the differ reads COALESCE(tenant) as tenant on a certified database (18.x step 7b-1 + 7b-2) - #24893

Draft
objectstack-fleet[bot] wants to merge 96 commits into
mainfrom
claude/issue-24583-7b12-index-plain-form

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Part of #24583

Clause-②: yes (widening). New DDL changes form.

  • Nothing is renamed, and no declared organization composite is dropped or rebuilt.
  • One index is re-keyed. The sys_setting row-identity index, which the metadata protocol rebuilds on every boot, keys the plain organization_id from the next boot on. This is probe-first and a pure relaxation, so it cannot fail on existing rows.
  • Some tenant columns are still nullable: every table of a database 18.0.0 created, and a table the ceremony reported. On those tables a newly declared organization unique no longer constrains NULL-organization rows, and the in-memory driver exempts a record with no organization.

Clause-② text corrected by the epic seat on 2026-10-20 (about 16:15Z) after the contract review's B1 (record 6293077099). It mirrors .changeset/24583-plain-organization-key-part.md at 3bb7ad6b1.

18.x step 7, parts 7b-1 + 7b-2 (claim 6289404450, governing record 6285122667). ⛔ This lands only after 7b-0 (claude/issue-24583-7b0-tenant-not-null-ddl) has merged and this branch has merged main again. Until then the diff carries 7b-0's commits too.

What changes

7b-2: new DDL keys the plain organization column (ADR-0131 D1: the tenant column is NOT NULL).

  • uniqueIndexesFromFields, normalizeDeclaredIndex (unique: 'organization') and legacyUniqueReplacements no longer mark the tenant key part NULL-safe. A field-level organization unique is UNIQUE (organization_id, f). buildIndexName is unchanged, so no index is renamed.
  • The same applies on every emitting path:
    • the sql-driver index emitters and the MySQL hash shadow, whose new shadows hash the plain column;
    • Turso, local and remote (it shares the generators);
    • memory-unique-constraint.ts, where a NULL organization now exempts the row like any other NULL key part;
    • os generate migration, which now emits the organization composite it used to list as NOT EMITTED (GLOBAL_TENANT_KEY removed);
    • the sys_setting row-identity migration: organization_id is plain and COALESCE(user_id, '') stays. Its duplicate query lists only rows the index refuses.
  • The duplicate pre-flight (probeNullSafeUniqueDuplicates, local and Turso remote) leaves out rows with a NULL plain key part. The index never refuses those rows.
  • __global__ as the autonumber counter key is untouched (7b-3's ground).

7b-1: the differ reads COALESCE(tenant) ≡ tenant, in both directions, where the tenant column holds no NULL.

  • diffManagedIndexes takes tenantKeyCertified. When it is set, the tenant key part's form drops out of the comparison identity. The column set, uniqueness and other key parts still have to match.
  • SqlDriver.detectTableIndexDrift sets it from two witnesses:
    • The tenant column is physically NOT NULL. This is self-proving, and 7b-0 guarantees it on fresh tables. The ceremony's SQLite rebuild and its PostgreSQL/MySQL ALTER produce it too.
    • The database is certified. The engine hands every driver a closure, ObjectQL.isOrganizationOwnershipCertified(): a verified sys_migration ownership row at the current shapeGeneration or above. This is the same reading the boot guard boots on, and it covers a birth marker or the ceremony's marker.
    • The driver asks for certification lazily, and only for a table that physically carries the COALESCE tenant key part. A failed read is reported once and read as "not certified". This is the fileColumnsMoved supply-seam precedent, so the driver never learns the ledger's vocabulary and there is no layering break.
  • The COALESCE form is still read: enforcedIndexKey, isSyncReproducibleIndex, isRuntimeManagedIndex and the shadow-key reader are unchanged. An input carrying nullSafeColumns is still honoured verbatim.

Measured

  • The ceremonied 18.0 fixture (ceremoniedFixture):
    • On SQLite, HEAD's own ceremony NOT NULL step rebuilds 62 tables through the drift apply door, and that rebuild re-keys their indexes plain. sys_email_template stays nullable (with sys_sso_provider and one rotation shard) and keeps its 18.0 COALESCE index, so on the fixture as-is the certification witness is what carries it.
    • PostgreSQL/MySQL get NOT NULL by ALTER and a Turso remote store not at all, so there every 18.0 index survives the ceremony. A SQLite store certified by an earlier 18.x ceremony keeps them too. The pin therefore also restores all 13 COALESCE indexes exactly as 18.0 wrote them.
    • On both shapes, HEAD's differ reports 0 recreate_index and 0 destructive drift. The 12 declared composites survive the boot byte for byte. sys_setting's index is rebuilt by its own runtime migration, which rebuilds it every boot anyway.
    • packages/qa/dogfood/test/upgrade-18x-index-equivalence.dogfood.test.ts: 7/7.
  • Fresh install: every organization unique index is plain, with 0 COALESCE and no index drift (same file).
  • Ablation, via scripts/ablation-replace.mjs on schema-drift.ts, comparing with the equivalence switched off:
    • ablation-dist-preflight showed the marker in 2 built files.
    • The fixture run went red: 2 failed / 5 passed, both drift legs, with 26 recreate_index lines in the output.
    • Restore: the blob equals HEAD, the dist marker is gone, and the tree is clean; the rerun gave 7/7.
  • MySQL hash shadow, live on MySQL 8.0.46 (a local container):
    • A fresh shadow's generation is unhex(sha2(concat(organization_id,0x1f,v),256)), which is plain.
    • An 18.0 COALESCE shadow is recreate_index/destructive when uncertified and gives no drift when certified.
    • A re-boot leaves the generated column's expression and the rows untouched.
    • No data migration and no column rebuild. A shadow is regenerated only when its index has already been dropped (stale); the column is derived, so no data moves.

Proof

  • R1, fresh pnpm dev -- --fresh on a random port: health 200, seeded-admin sign-in 200, 0 error lines, no schema-drift line.

  • R2 + R3: upgrade-18x-harness with the new equivalence file gave 15/15; organization-ownership-boot-refusal-order gave 4/4.

  • 18.0.0-born: measured live on the published 18.0.0 (os migrate plan: 0 changes over 73 tables), and pinned as the third leg, 10/10. On the merged tree with test(dogfood): fixture-18.0.0 — the upgrade harness boots a database the published 18.0.0 created #24911's fixture-18.0.0 leg, the dogfood files give 26/26.

  • R4: built CLI run-dev.js serve under OS_TENANCY_POSTURE=isolated, on a throwaway app declaring the real @objectstack/organizations (invite-only membership).

    • Banner reached, Tenancy: isolated, Organizations mounted.
    • Health 200 and discovery 200.
    • 0 error lines and 0 refusals.
    • In the store: 14 organization unique indexes, 0 COALESCE, and 0 NULL rows across 30 tables carrying organization_id.
  • Step 4's 7 walled e2e cases (OS_TEST_TIERS=nightly): 7/7.

  • Suites:

    Package Result
    driver-turso 2442 passed
    driver-memory 1757
    metadata-protocol 20831 + 1
    objectql 8501
    cli --project unit 4635
    driver-sql 3705 passed, 3 failing (see below)

    The integration files touched were run alone. typecheck is clean on all six packages.

  • driver-sql's 3 failures are not this change. Each one inserts an organization-less row into a fresh table, which 7b-0's NOT NULL now refuses: sql-driver-21227-create-answers-stored-row (2) and 7b-3's sql-driver-organizationless-counter-refusal (1). Measured on the base merge of main and 7b-0 without this change, the first two already fail; the third arrived with 7b-3. They belong to 7b-0's next merge of main.

  • node scripts/pm/dispatch-gates.mjs --commands: 105 families, each exit captured before any pipe. The final-head results are in the report.

    • check-engine-split-ratio and check-plugin-teardown-shape --self-test refused on the shallow clone, then passed after deepening.
    • check:durability-log-level and check:query-options-erasure went red on an earlier head and were fixed here.
  • check:generated: exit 0.

  • check-governed-merges --branch: Tier H by size.

Size

The governed-merges check reads 5,532 changed lines while 7b-0's commits ride along, so this is Tier H by size and it is not split. This part's own commits come to 2,465 lines: 478 production, 1,968 tests, 19 changeset. There are 0 fixture lines; no fixture was regenerated.

维护者速读(草稿)

  • 改了什么:新建的组织唯一索引改用普通的 organization_id 列,不再用 COALESCE(organization_id, '__global__')。旧库里已有的 COALESCE 索引,只要库已认证(出生标记或仪式标记)或租户列已 NOT NULL,差异检测就视为同一个索引,不重建、不改名。唯一例外是 sys_setting 的行标识索引:元数据协议每次启动都会重建它,下次启动起改用普通列(先探测再替换,只放宽、不会因已有数据失败)。
  • 为什么改:ADR-0131 D1 规定租户列 NOT NULL,没有组织为空的行,__global__ 桶已无意义;不加等价判断的话,每个升级上来的库每次启动都会被提示「删掉重建」13 个唯一索引。
  • 风险与代价(含回滚):租户列仍可空的表上(用 18.0.0 新建的库的所有表、仪式没收紧的表),新建的组织唯一索引不再约束组织为空的行(与其他可空键一致)。除 sys_setting 行标识索引外,已有索引一律不动,回滚即还原本 PR,不涉及数据迁移;MySQL 实测无需重建列。
  • 席位意见:
  • 你要做的:因体量超过 3000 行(含 7b-0 的提交)属 Tier H:需一次授权批准,且须在 7b-0 合并��本分支再合 main 之后落地。

Acceptance notes

  • The tightening recreate_index of ADR-0120 D4 (bare composite → NULL-safe) is no longer produced by any generator. Its apply path and pre-flight remain for an op that already carries nullSafeColumns, and the Turso suites pin them that way.
  • On an uncertified database with a nullable tenant column, an 18.0 COALESCE index is still reported as a destructive recreate_index toward the plain key.
    • The finding's message names os migrate organization-ownership as the remedy, never os migrate apply --allow-destructive (B3, pinned in sql-driver-tenant-key-equivalence.test.ts).
    • No unattended path applies it: boot reconciles only safe changes, and the artifact gate refuses.
    • Such a database is refused at os serve until the ceremony certifies it.
  • os generate migration emits the tenant column nullable, while the driver's fresh table is NOT NULL (7b-0). The generated composite is now the platform's own constraint, which is stronger than the nothing it emitted before, but the column's nullability still differs from the driver's. That is 7b-0's ground and is not changed here.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NnXVu3vF2TKFLMi5Me4CUd


Generated by Claude Code

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>
…fault Organization

Claude-Session: https://claude.ai/code/session_018EszgtA3V2KXBX5tf59Hi4
Co-authored-by: Claude <noreply@anthropic.com>
…n organization key part (ADR-0131 D1)

Claude-Session: https://claude.ai/code/session_01NnXVu3vF2TKFLMi5Me4CUd
Co-authored-by: Claude <noreply@anthropic.com>
…the safe drift that survives the plain organization key part

Claude-Session: https://claude.ai/code/session_01NnXVu3vF2TKFLMi5Me4CUd
Co-authored-by: Claude <noreply@anthropic.com>
…t-null-ddl' into claude/issue-24583-7b12-index-plain-form
…12-index-plain-form

# Conflicts:
#	packages/plugins/plugin-auth/src/sso-register-platform-admin-gate.test.ts
#	packages/qa/dogfood/test/parent-derived-write-refusal-not-visible.dogfood.test.ts
#	packages/qa/dogfood/test/predicate-write-unreadable-not-matched.dogfood.test.ts
#	packages/qa/dogfood/test/sharing-rule-org-less-caller.dogfood.test.ts
#	packages/qa/dogfood/test/write-door-unreadable-is-not-found.dogfood.test.ts
#	packages/runtime/src/default-organization-statements.golden.test.ts
#	packages/services/service-automation/src/sys-user-default-row-scope.integration.test.ts
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review (delta re-review after FAIL at de01287)

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d4e977b874c974d501607bcd7b45621341c4e5c6
Local-runs: none
Scope: .changeset/24583-plain-organization-key-part.md, packages/drivers/driver-sql/src/schema-drift.ts, packages/drivers/driver-sql/src/sql-driver.ts, packages/drivers/driver-sql/src/sql-driver-tenant-key-equivalence.test.ts, packages/objectql/src/engine.ts, packages/objectql/src/adr0131-tenant-key-certified-supply.test.ts, packages/plugins/plugin-security/src/grant-permission-set-name.test.ts, packages/cli/test/dev-standalone-self-heal.integration.test.ts, packages/qa/dogfood/test/upgrade-18x-index-equivalence.dogfood.test.ts + read to judge them: packages/qa/dogfood/test/pre-18-tenant-column.testkit.ts, packages/qa/dogfood/test/upgrade-harness/populate-fixture.ts, packages/qa/dogfood/test/upgrade-harness/fixture-18.0.0/database.sql (origin/main, #24911), packages/qa/dogfood/test/upgrade-18x-harness.dogfood.test.ts, packages/plugins/plugin-security/src/pre-18-tenant-column.testkit.ts, packages/cli/src/utils/schema-migrate.dev-self-heal-fence.integration.test.ts, packages/drivers/driver-sql/src/index.ts, packages/drivers/driver-turso/src/turso-driver.ts, packages/spec/src/system/migration.zod.ts, packages/runtime/src/organization-ownership-boot-guard.ts, AGENTS.md

Reviewed: the dev's four own commits since de01287 (9ef44b5, 3bb7ad6, c1f0005, d4e977b; 9 files, the delta in delta-own.diff), read in a detached worktree of the head d4e977b874, against the previous record's B1 to B4; the live PR body (the seat's B1 correction); the real 18.0.0 fixture on main; the check-runs, check-suites, combined status and workflow-run history of the head and its neighbours through the GitHub API. The merges of main and of 7b-0's branch in the range (f56c207, f60f1ce, 4e5e331, 39d8b9d) were ignored except where a hunk depends on them. Nothing built, run or re-run; no job log was reachable from this seat (the results-receiver and blob-storage URLs answer 403 through the proxy), so CI reads come from annotations and conclusions only. Note on the moving branch: the branch tip is now 4dadd7ea45 (a merge of main and of 7b-0's branch after this head); among this part's files it changes only sql-driver.ts, by main's #24920 (hunks at 4875, 15522 to 15561, 21018 to 21070: PG introspectSchema), outside every region judged here; the changeset, schema-drift.ts, engine.ts and the four tests are byte-identical between this head and the tip.

① Derived judgments

1. B1 — the changeset is now a true declaration. CLOSED.

  • (a) The sys_setting re-key is stated: .changeset/24583-plain-organization-key-part.md:17 reads "One index is re-keyed: the sys_setting row-identity index, which the metadata protocol rebuilds on every boot, keys the plain organization_id from the next boot on (probe-first; a pure relaxation, so it cannot fail on existing rows)". That is what ensureSysSettingIdentityIndex through probeThenReplaceIndex does (judged in the previous record, unchanged in this delta).
  • (b) The 18.0.0-born case is stated: line 18 lists "every table of a database 18.0.0 created" among the still-nullable tables, and line 19 reads "A database 18.0.0 created from empty is certified by its birth marker yet keeps nullable tenant columns: its existing indexes are kept unchanged, and a new organization unique declared on one of its tables constrains only rows that carry an organization". The real artifact agrees: fixture-18.0.0/database.sql:607 (main, test(dogfood): fixture-18.0.0 — the upgrade harness boots a database the published 18.0.0 created #24911) holds the ownership row with verified_at set, blocking 0 and details {"birth":{"shapeGeneration":1,"platformVersion":"18.0.0",...}}; its 65 organization_id column definitions carry no NOT NULL; 13 of its 17 organization unique indexes are in the COALESCE form.
  • The rest of the bullet: "the finding is reported, and it names os migrate organization-ownership as its remedy" is true of schema-drift.ts:2603 to 2610 (judgment 3). "Nothing is renamed" holds (buildIndexName untouched). "no declared organization composite is dropped or rebuilt" is true of what the boot, os migrate plan and os migrate apply do; see R1 for the one precision it leaves open (the SQLite ceremony's table rebuild). Nothing else in the changeset reads false against the code.
  • The PR body's Clause-② line mirrors lines 17 to 19 in three bullets: no rename and no composite rebuilt; the sys_setting re-key, probe-first, a pure relaxation; the nullable tables (18.0.0-created, ceremony-reported) where a newly declared organization unique no longer constrains NULL-organization rows and the memory driver exempts a record with no organization. The body's nullable list is a subset of the changeset's (it omits the uncertified 17.x / 18.0 store, the rotation shard and the Turso remote table), which is not a contradiction. The body's correction note carries a literal template placeholder (R2).

2. B2 — the "not certified" prose now says what the code does. CLOSED.

  • sql-driver.ts:20414 to 20421 (setTenantKeyCertifiedResolver docblock): "the answer is 'not certified': the two forms then stay distinct constraints, and the differ REPORTS the 18.0 index as a destructive recreate_index. That report is never applied unattended — the boot reconcile applies safe entries only, the artifact gate refuses it, and os migrate apply needs --allow-destructive". sql-driver.ts:20450 to 20454 (the catch in isTenantKeyCertified) says the same. engine.ts:12212 to 12215 (the declined-resolver arm) says the same. The objectql test header (adr0131-tenant-key-certified-supply.test.ts:17 to 19) says "the differ reports a destructive recreate_index, which is never applied unattended".
  • Each gate named is the one the previous record verified (reconcileAndWarnDrift safe-only, the artifact gate, the --allow-destructive door).
  • A repo-wide search of the head for "never proposes a rebuild" and "propose(s) a rebuild" finds nothing; the remaining "on a guess" hits in the touched files are pre-existing and about other things (schema-drift.ts:2320, the unresolved hash-shadow carrier; sql-driver.ts:12692, a builtin column), and both are correct as written.

3. B3 — the uncertifiedTenantKeyOnly arm. CLOSED.

  • Where it fires (schema-drift.ts:2568 to 2575): tenantKeyCertified !== true, a tenant field, p.unique === e.unique, the physical enforced key NULL-safe on the tenant field, the expected key not, and canonicalIndexKey equal on both sides once the tenant field is removed from each side's nullSafeColumns. A different column set makes the canonical keys differ (generic arm); a different uniqueness fails p.unique === e.unique (generic arm); a COALESCE on another column, say workspace_id with tenantField organization_id, leaves the tenant field out of the physical nullSafeColumns (generic arm); a second NULL-safe part on one side only survives the filter and makes the keys differ (generic arm). So it fires exactly for the uncertified tenant-key-only difference. It also fires for tenantKeyCertified undefined, which no caller passes.
  • The message's "'organization_id' is nullable" cannot print on a NOT NULL table: diffManagedIndexes has one caller, detectTableIndexDrift (sql-driver.ts:13934; no other non-test call site in the repo). It sets tenantKeyCertified = tenantColumnNotNull || (carriesNullSafeTenantKeyPart(physical, tenantField) && await this.isTenantKeyCertified()) (13931 to 13933), with tenantColumnNotNull from detectTableDrift (13895: cols.some(...) with c.name === tenantField && c.nullable === false), and cols from introspectColumns (13854; 21059 to 21107: nullable: info.nullable !== false from knex columnInfo() on every dialect). The Turso remote face overrides introspection with remoteIntrospectedColumn, whose nullable comes from notnull (turso-driver.ts:704), and inherits detectTableDrift. A physically NOT NULL column therefore reaches the differ as tenantKeyCertified true, and the arm is dead for it. A column whose nullability a dialect did not report reads as nullable everywhere in the differ, which is consistent with the message.
  • The op is unchanged: type: 'recreate_index', category: e.unique ? 'destructive' : 'needs_confirm' (2590 to 2602); the arm selects the message only, as its comment says (2565 to 2567). For a unique index it stays destructive.
  • The pin (sql-driver-tenant-key-equivalence.test.ts:76 to 88) asserts the ceremony is named, "not certified" appears, the "Do not rebuild it with ..." sentence appears, and the old remedy sentence is absent; its control (83 to 87) is a real redefinition (ORG, code, region) which keeps the --allow-destructive remedy and does not name the ceremony. The neighbouring pins keep the uncertified pair destructive (70 to 74), the workspace_id tenant control (95 to 97) and the column-set control under certification (99 to 102).
  • A nit, not a finding: the message's "Do not rebuild it with os migrate apply --allow-destructive" would be odd on a non-unique index (needs_confirm, applied by a plain apply), but no generator emits a NULL-safe tenant key part on a non-unique index (uniqueIndexesFromFields, normalizeDeclaredIndex with unique: 'organization'), so the case does not arise.

4. The two re-pins — neither weakens an assertion.

  • c1f0005, plugin-security grant-permission-set-name.test.ts. Before (de01287): three NEGATIVE pins in which the NULL-organization twin was refused 409 — a second engine insert by ADMIN and by SYS after an ADMIN insert with organization_id: null, then the revoke; a two-row batch with no organization storing nothing plus a raw-SQL NULL twin refused; an update re-naming a set into a NULL-organization duplicate refused. After (head): line 314 to 319 asserts the NULL twin is NOT refused on the pre-18 nullable column (ups_p1 through the engine as ADMIN, ups_p2 through raw knex, both present); 321 to 340 refuses an ORGANIZATION duplicate on the batch path (nothing stored) and on raw SQL (UNIQUE constraint failed); 342 to 352 refuses an ORGANIZATION duplicate made by an update, row untouched. The engine-path organization duplicate (300 to 312, SYS after ADMIN, then the revoke leaves none) and the CONTROL (354 to 370) are unchanged from before. The docblock (9 to 14) states the widening. Is it the declared widening? The harness is a pre-18 store by construction: boot() calls relaxEveryTenantColumn after syncSchemas() (line 101), which goes through each driver's own relax_not_null drift door (the testkit, 15 to 30); on that nullable table the plain index exempts the NULL twin exactly as the changeset's "What widens" bullet declares, and a fresh 7b-0 table would refuse the NULL row at NOT NULL, so the exemption exists only there. Positive controls: an organization duplicate is still refused on engine insert, batch insert, raw SQL and update, and the revoke still leaves none — every path the old pin covered. Two observations, neither a weakening: the old pin iterated ADMIN and SYS as the second engine writer and the organization-duplicate pin has SYS only (ADMIN is covered on the update path; the index refuses regardless of writer); and the positive assertion of the widening inserts the twin by raw knex rather than through the engine (the engine has no unique pre-check of its own, engine.ts maps the driver's error only at 8238 and 16557, so the engine path admits it too; the test's title scopes the claim to the index).
  • d4e977b, cli dev-standalone-self-heal.integration.test.ts. Before: boot 1 asserted the fresh index is NULL-safe; boot 2 staged a bare composite (DROP + CREATE plain) and asserted the restart put COALESCE back with "auto-reconciled recreate_index on scaf_item"; boot 3 asserted production printed "index '...' tightens", applied nothing, and left the bare index. After: boot 1 asserts the fresh index is the plain composite with no COALESCE and that no legacy single-column UNIQUE exists (280 to 285); boot 2 stages uniq_scaf_item_qa_code UNIQUE (qa_code), the uniq_ arm of legacyUniqueIndexNames (schema-drift.ts:2129 to 2131; exported at driver-sql index.ts:110), and asserts the restart dropped it, the composite survived plain, and the output holds "[schema-drift] auto-reconciled replace_unique_index on scaf_item.qa_code" (287 to 293; the driver's line is auto-reconciled ${op.type} on ${table}.${column} at sql-driver.ts:14293); boot 3 asserts production printed "a legacy platform-wide UNIQUE index (uniq_scaf_item_qa_code)" (schema-drift.ts:2471), no auto-reconciled line, the legacy index still present and the composite plain (300 to 306). Why the staging moved: under the plain expectation a bare composite MATCHES (comparisonKey equal), and the D4 tightening arm needs an expected nullSafeColumns the generators no longer emit, so the old staging yields no drift at all and the old restart leg could neither pass nor measure anything. The new staging carries the safe op the generators still produce (replace_unique_index, category: 'safe', schema-drift.ts:2460), the same one schema-migrate.dev-self-heal-fence.integration.test.ts stages (79, 203, 240, 253; re-staged at de01287, in the previous record's scope). Does it still prove the fence? cli: a plain os dev (standalone stack) never runs autoMigrate 'safe' — the documented dev self-heal applies only to host configs, while plan and drift lines say "auto-applied at boot" #21733's defect was the standalone stack never applying the self-heal's safe subset; boot 2 fails on exactly that defect (the legacy index would survive and legacySql be defined), and boot 3 fails if production applies it. It has not moved to a case that cannot fail; each leg now asserts strictly more (legacy presence or absence, composite survival, and the output line). The docblock (36 to 45) explains the move.
  • Caveat for both: CI never ran on this head (item 7), so the re-pins' green is unmeasured here; the tip's Test Core shards were still running at review time.

5. The 18.0.0-born dogfood leg (9ef44b5) models the real store. B4 CLOSED as to the leg.

  • What it builds (upgrade-18x-index-equivalence.dogfood.test.ts:180 to 257): a HEAD birth through bootFixture, which composes OrganizationOwnershipBootGuardPlugin (populate-fixture.ts:24, 67), so the store receives the birth marker at ORGANIZATION_OWNERSHIP_SHAPE_GENERATION 1 (boot-guard.ts:492 to 502); then relaxTenantColumns over every table holding an organization unique index except sys_setting, through the driver's relax_not_null door (on SQLite rebuildSqliteTablePatched, sql-driver.ts:14646 onward, which re-creates the declared indexes from metadata, plain); then each organization key part re-spelled to COALESCE(organization_id, '__global__') exactly as 18.0.0 wrote it (214 to 219); then a HEAD boot.
  • What it asserts: the marker's verified_at is set and details.birth.shapeGeneration is 1 (234 to 238); at least 10 indexes were re-spelled (242); every re-spelled table's organization_id is nullable by pragma_table_info (243 to 246); zero recreate_index and zero destructive entries from detectManagedDrift over the store serving the ledger (249 to 251, helper 96 to 101); every COALESCE index byte-identical after the boot (253 to 256).
  • Against the real fixture-18.0.0 (main, test(dogfood): fixture-18.0.0 — the upgrade harness boots a database the published 18.0.0 created #24911; also in this head's tree through the main merge at 81e6edd): the marker row at database.sql:607 is {"birth":{"shapeGeneration":1,"platformVersion":"18.0.0",...}} with verified_at set and blocking 0, the same shape HEAD's birth writes, so organizationOwnershipShapeGenerationOf (migration.zod.ts:243 to 255) reads 1 for both; 13 CREATE UNIQUE INDEX lines key COALESCE(organization_id and 4 more key organization_id plain as an ordinary column (sys_business_unit, sys_member, sys_team, sys_user_position); all 65 organization_id column definitions are nullable. The leg's three premises (birth marker at generation 1, nullable tenant columns, COALESCE indexes) are the real artifact's. The real fixture's own leg (upgrade-18x-harness.dogfood.test.ts:306 to 338) asserts the birth marker and that HEAD's schema plan holds no destructive change; it does not assert zero recreate_index or byte identity, which this leg adds on the same shape. The two together cover the case the previous record found missing.
  • Prose only: the leg's comment says "10 of 14 organization uniques in the COALESCE form" measured on the published 18.0.0, while the showcase fixture holds 13 of 17; the assertion is "at least 10", so it holds either way. The leg relaxes only the tables under test, and the "no tenant column here is NOT NULL" claim is asserted per re-spelled table, which is what the equivalence needs.

② Semver level

minor with Clause-②: yes (widening) still holds. The delta adds no behaviour: one message text (B3, on an op that keeps its type and category), code and test prose (B2), changeset and PR-body prose (B1), two re-pins and one new dogfood leg. The widening declaration is now wider than before (the 18.0.0-born store and the ceremony-reported table are named), which is still a widening and not a narrowing; nothing accepted before is refused now. The six package bumps are unchanged and match the six packages the diff changes.

③ Boundary flags

  • R1 — not blocking, precision. Changeset line 17's "no declared organization composite is dropped or rebuilt" is true of the boot, os migrate plan and os migrate apply. It leaves open the SQLite ownership ceremony: its NOT NULL step rebuilds each table it tightens through rebuildSqliteTablePatched (sql-driver.ts:14646 onward, "declared indexes are recreated from metadata afterwards"), and the composites come back keyed plain — the PR body's own Measured section says so ("that rebuild re-keys their indexes plain", 62 tables). The name is kept, the constraint is identical on the now NOT NULL column, it is operator-run, and the rebuild belongs to the NOT NULL step, not to this diff. Suggested clause for the next edit of the changeset (no behaviour change): "by the boot, os migrate plan or os migrate apply; on SQLite the ownership ceremony's NOT NULL step rebuilds the tables it tightens, and their composites come back plain, enforcing the same constraint".
  • R2 — not blocking, the seat's. The live PR body's correction note reads "corrected by the epic seat at 2026-10-20T16:33Z": a literal template placeholder.
  • R3 — not blocking, attribution gap. The last completed CI before this head (3bb7ad6) also had packages/plugins/plugin-webhooks red in Test Core 2/6, which the dev's attribution does not mention. This part's delta touches no plugin-webhooks path; the candidates are 7b-0's NOT NULL and main's fix(plugin-webhooks): report a webhook delivery the outbox could not record at error, once per webhook #24862 (plugin-webhooks outbox); owner undetermined from this seat, not this part. The tip's Test Core 2/6 will say whether it persists.
  • R4 — not blocking, measurement. No GitHub Actions check-suite exists for d4e977b874 (check-runs 0; check-suites: five third-party apps queued with 0 runs; combined status pending with 0 contexts), and the workflow-run history shows no run for c1f0005 or d4e977b either; the pushes were superseded before Actions picked them up. So the two re-pins have no CI verdict on this head. The seat should read the tip 4dadd7ea45's Test Core 2/6 (cli, plugin-security, plugin-webhooks) and 5/6 (plugin-approvals) as the measurement; at review time its Lint, Type Check and all six Test Core shards were in progress and every completed check was green.
  • CI attribution (item 7), from the last completed CI on the branch before this head, 3bb7ad6: Test Core 2/6 red — packages/cli, packages/plugins/plugin-security, packages/plugins/plugin-webhooks pnpm run test exited 1; Test Core 5/6 red — packages/plugins/plugin-approvals exited 1; everything else green, including Lint & Type Check and the 18.x upgrade harness. Owners: cli — this part's own, fixed by d4e977b (the old staging yields no drift under the plain expectation, so the old restart pin could not pass); plugin-security — this part's own, fixed by c1f0005 (the old NULL-twin refusal pins cannot pass against the plain index); plugin-approvals — 7b-0's (NOT NULL constraint failed: sys_user.organization_id per the dev; 7b-0 then added 5bd3515 "main's two new lean fixture engines hold a Default Organization", merged into this branch at 33feb16, beyond this head); plugin-webhooks — 7b-0 or main, see R3. The Type Check · workspace red on 9ef44b5 was cleared by main's b5442cd and is green from 4e5e331 on. Job logs were not reachable from this seat; the package attribution rests on the annotations and on the reading of the tests above.
  • Merge-of-main note. At this head the branch conflicted with main: 7a-2 feat(core,plugin-security)!: 18.x step 7 part 7a-2 — a grant applies only in its own organization; the organization-less grant readers retire #24872 (94842ba) and fix(cli): os migrate apply on an empty database writes the organization-ownership birth marker, so the first os serve starts #24875 (19484af) landed after its last merge of main (39d8b9d). Expected under the claim's merge gate, not this record's finding. The tip 4dadd7ea45 has since merged main with both and 7b-0's branch; the API reports the PR mergeable, state blocked on pending checks.
  • Observation carried from the previous record, unchanged: the certificate at generation 1 does not prove ADR-0131 D1's "no nullable tenant column anywhere" (the 18.0.0-born store and the ceremony-reported tables stay nullable); the changeset now says so plainly. For 7b-0 and the epic seat.

Implemented-by: claude/issue-24583-7b12-index-plain-form
Reviewed-by: session_01Y5583bs2RDd53MH6kM1ca3

VERDICT: PASS

…n stamp alone — the 7b-0 × main textual merge had kept both keys

Claude-Session: https://claude.ai/code/session_01NnXVu3vF2TKFLMi5Me4CUd
Co-authored-by: Claude <noreply@anthropic.com>
os-steve pushed a commit that referenced this pull request Oct 20, 2026
… into 7b-0

Conflicts in seven fixture files take #24893's resolution (4dadd7e): where
7a-2 now writes the grant with an organization the pre-18 relax goes; where
main still lands an organization-less row the relax stays.
sharing-rule-criteria-required takes main's version verbatim (both sides had
added the organization key). plugin-webhooks' new real-SqlDriver case gets the
lean engine's Default Organization.

Claude-Session: https://claude.ai/code/session_018EszgtA3V2KXBX5tf59Hi4
Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Oct 20, 2026

This branch has not been deployed

No deployments
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.

2 participants