Repository navigation
[Decision] C8: a certified database's leftover organization-less rows — block certification, or go dark when the NULL arms retire #24352
Description
Filing gate ②: a decision only the maintainer can make. Reader who acts: the maintainer rules; the epic PM (epic:#15194, session_01Y5583bs2RDd53MH6kM1ca3) executes it in C8 (#15212). Dedupe: semantic search on objectstack-ai/objectstack, all states, "advisory organization-less rows after NULL arm retirement certified database unattributable rows block ceremony" → 6 hits (#15194, #15212, #24343, #24282, #23872, #17300); none asks this question.
Blocks: C8's stages 8c-1 (the driver arms), 8c-2 (grant and holding readers) and 8c-3 (sharing, approvals and settings readers). It does not block 8a, 8b-0 or 8b.
维护者速读
一句话问题: v18 删掉「空归属=所有租户可见」之后,升级仪式也找不到主人的那些旧数据行怎么办——是在升级时就拦住、要求运维逐行指定归属(或删除),还是升级照常通过、这些行从此对所有租户隐身、不再授予任何权限?
你要做的: 选一个字母。推荐 C。
| 选项 | 做什么 | 客户感受 | 工作量 |
|---|---|---|---|
| A 升级照过,旧行隐身 | 仪式照常认证;C8 删掉空归属分支后,这些行任何租户都读不到,岗位持有、授权都不再生效;启动报告逐行列出,附手工补救办法 | 升级不受阻;但少数人可能悄悄失去某个岗位或授权,只有看启动报告才知道 | 约 100–200 行,并入 8c(只多出报告文案) |
| B 按库切换 | 代码里同时保留新旧两套规则,按每个库是否已到「零空归属」切换 | 无感,但 v18 整个生命周期两套租户规则并存 | 多 1 个阶段,约 1–1.5k 行;删除推迟到下一个大版本 |
| C 升级时拦住 | 仪式把「找不到主人的行」从提示(advisory)改为阻断(blocking):没逐行处理完,库就拿不到认证,v18 不对它服务;报告逐行给出补救办法(指定一个组织,或删除) | 有这类行的运维必须先处理再升级,一次性的明确动作;之后数据库保证零空归属,C8 的删除对所有库都安全 | 约 0.5–1k 行,一个仪式 PR;先例 #23988(728 行,同一处 post-check) |
Background
- C8 retires both
orWhereNullarms (packages/drivers/driver-sql/src/sql-driver.ts:15601,:15614onorigin/maindd72a047) and the NULL readers in the authorization resolver, sharing, approvals and settings (8-M census, in the epic seat's session, on51ad8e4f). - The ownership ceremony certifies a database while some NULL rows remain, counted as
advisory: fate 4 "reported" rows, and fate-3 rows whose anchor is missing under a wall (for example asys_user_positionrow with no business unit). Code:packages/cli/src/utils/organization-ownership-apply.ts:134("advisory: named and counted, its rows still NULL, never deleted"),:498–:501. - After C8, such a row is reachable by no tenant-scoped read and confers no grant. Nothing tells the operator except the report.
- Since fix(cli,runtime): the 17 → 18 ownership ceremony leaves no row without an organization on a single-posture database, its prescribed boot writes nothing before the guard refuses, and an unverified ceremony is not served #24325 (
51ad8e4f), an unverified ceremony is not served at boot. So under C, a database that keeps such rows does not start on v18 until they are resolved.
Governing text
- ADR-0131 D13 (
docs/adr/0131-total-organization-ownership-no-null-organization-id.md:681): "When D10 reports zero NULL rows on every table: bothorWhereNullarms and any sibling driver's equivalent; … the NULL readings inresolve-authz-context.ts…" - ADR-0131 D10 fate 4 (
:562–:563): "a row whose owner cannot be derived is left NULL and listed at boot, per table, with counts and the remedy. ⛔ Never guessed, never deleted. The NOT NULL constraint lands on a table only when it reports zero NULL rows." - ADR-0131 D14 (
:716): "The driver arms are removed in the same major, after C7." - Ruling β (6216760903 on refactor(plugin-security,platform-objects,spec): retire the catalog seeders, the per-organization catalog machinery and the four catalog objects; Setup creation is an environment write under
singleand refused under a wall (ADR-0131 D2/D3/D5/D13) #15204), item 1: "the marker keeps meaning 「zero NULL rows on every table」"; item 2: "Nothing else is exempted: any other NULL row keeps blocking both the marker and stage 9." - The os migrate organization-ownership never certifies a database whose packages declare sharing rules: it waits on the declared-rule-mirrors departure (D2) while the same release's boot still seeds them #23948 hand-off (6220693269 on C3 stage 9 (#15204): declared sharing rules read from the registry, bootstrapDeclaredSharingRules retires, and the ceremony deletes the declared-rule mirrors; the verified marker follows (director ordering ruling) #23952), reading 1: on
main, unattributable rows "were already counted asadvisory, neverblocking… Whether the ruling means to make them block is its owner's reading."
Why it is not settled: D13 gates the retirement on a per-database zero-NULL report; D14 gates it on card order inside one release. A code release cannot satisfy D13 for a database that keeps advisory rows, and β item 2's literal reading ("any other NULL row keeps blocking the marker") disagrees with the code, which counts those rows as advisory.
Protocol: none of the options changes a packages/spec schema. C changes the ceremony's verdict; A and B change what the runtime reads.
Premises (each with a re-check command; run on current origin/main)
- Both arms are on
main.git grep -n "orWhereNull(field)" origin/main -- packages/drivers/driver-sql/src/sql-driver.ts→ 2 hits. Positive control:git grep -c "applyTenantScope" origin/main -- packages/drivers/driver-sql/src/sql-driver.ts≥ 1. - The ceremony certifies with advisory rows.
git grep -n "advisory: named and counted" origin/main -- packages/cli/src/utils/organization-ownership-apply.ts→ 1 hit. Control:git grep -c "remainingFor" origin/main -- packages/cli/src/utils/organization-ownership-apply.ts≥ 1. - An unverified ceremony is not served.
git log --oneline -1 51ad8e4fnames fix(cli,runtime): the 17 → 18 ownership ceremony leaves no row without an organization on a single-posture database, its prescribed boot writes nothing before the guard refuses, and an unverified ceremony is not served #24325; readpackages/runtime/src/organization-ownership-boot-guard.tsonorigin/main.
Options
- A — certify with leftovers; they go dark at C8. Business meaning: like closing a shared drive and leaving the unlabelled folders in a locked room — nothing is deleted, but nobody can reach them until someone labels them.
- B — per-database switch. Business meaning: two locks on the same door for the whole of v18, chosen per customer.
- C — leftovers block certification. Business meaning: the move-in inspection refuses to sign off until every unlabelled box is labelled or thrown out by the owner.
Recommendation: C; fallback A
- 项目长远合理性(定调): 两年后的平台应当是「数据库里不存在没主的行」,认证标记就等于这句话(β 第 1 条原话「the marker keeps meaning zero NULL rows on every table」)。C 让标记名副其实,C8 删除空归属规则时对所有库都成立,代码里只有一套租户规则。A 让「已认证」的库仍带着隐身数据,标记的含义打了折扣。B 是 v18 全程两套规则,属于临时补丁。主流做法(Salesforce 的 org 隔离、多租户 SaaS 的 tenant_id NOT NULL)都是数据先归位、再上约束,不留无主行。
- 实际业务需求: 今天撞上的人很少:
single下仪式会把所有可归属的行归到默认组织;你的 hotcrm 库实测只剩 11 行共享规则副本,stage 9 会删掉。真正会有这类行的是带墙的部署(cloud),而 cloud 冻结在 v17(「cloud 冻结在 v17 没问题」),C10 迁移时才会遇到。C 的代价是:这类运维在升级时多一步明确操作。 - 防 AI 写代码犯错: C 是响亮拒绝——认证不过,报告逐行写明怎么处理。A 出错时是静默的:某人的岗位、授权在升级后悄悄失效,只能从启动报告里发现,排查的人(包括 AI)很难把症状和原因连起来。
- 创业阶段不扩散: A 最省(只加文案);C 多一个约 0.5–1k 行的仪式 PR,不加新工具(补救是运维手工指定归属或删除),也不新增门禁;B 最费,且把删除推到下一个大版本。
自检:只看①选 C;②③④ 是否翻转:否——② 拉动很小,只影响分期、不改方向;④ 让 A 更省,工作量差 3 倍以上(约 0.2k 对 0.5–1k 行),但①的读数并不相近,按规则不翻字母,这里点名说明。
置信缺口: 带墙的真实库里到底有多少这类行,本会话测不到(cloud 仓不在本会话、cloud 在 v17)。C 的报告文案要让 C10 的迁移者一眼看懂怎么处理。
After the ruling
- C: one ceremony PR (cli lane surface, dispatched by the epic seat as C8 stage 8c-0, before 8c-1): unattributable rows count as
blocking; the post-check names each table, the row count and the remedy (assign an organization, or delete by hand); pins for single and walled fixtures. Then 8c-1/8c-2/8c-3 retire the arms and readers for every database. - A: 8c-1/8c-2/8c-3 retire them for every database; the post-check and the boot report add one sentence: such rows are reachable by no tenant and confer nothing after v18, with the same remedy.
- B: a new stage adds the per-database switch, both paths pinned; the retirement moves to the next major.
os-decision-facets
- ① 项目长远合理性:C 缩小特例——认证标记等于零空归属,C8 删除后只有一套租户规则;B 扩大特例(两套规则并存);A 留下「已认证但仍有无主行」的例外。
- ② 实际业务拉动:今天
single下几乎为零(hotcrm 只剩 stage 9 要删的 11 行副本);带墙部署(cloud)在 C10 迁移时会遇到,数量本会话测不到。 - ③ 防 AI 犯错:C 是响亮拒绝、逐行给补救;A 是静默失效(岗位与授权悄悄不再生效);B 让两套规则长期并存,最易写错。
- ④ 创业阶段不扩散:A 最省;C 多一个约 0.5–1k 行的仪式 PR、不加工具不加门禁;B 最费并推迟收尾。
Prior rulings read: "zero NULL" "advisory" "orWhereNull" "D13" on #15194, #15204, #15212, #23952 threads → 3 hits (6216760903 β items 1–2; 6220693269 reading 1; 6100442578 gate paragraph); ADR-0131 D10, D13, D14; thread: 6216760903.
Related: #15212 (C8), #15211 (C7, closed), #23952 (C3 stage 9), #24282 / PR #24325, #23948 / PR #23988, objectstack-ai/cloud#1979 (C10).
Activity
objectstack-fleet commentedon Oct 18, 2026
Ruled: C — unattributable rows block certification · 2026-10-18T15:19Z
The maintainer answered this card in the epic seat's chat (session
session_01Y5583bs2RDd53MH6kM1ca3), choosing the option labelled, verbatim: 「C 升级时拦住 (Recommended)」. Recorded by the epic PM (os-steve). Freshness gate: the body was not edited after filing, and no comment preceded this one.What it binds:
advisory. A database that keeps one does not certify, and since fix(cli,runtime): the 17 → 18 ownership ceremony leaves no row without an organization on a single-posture database, its prescribed boot writes nothing before the guard refuses, and an unverified ceremony is not served #24325 an unverified ceremony is not served.Execution (plan 6251466182 on #15212):
objectstack-fleet commentedon Oct 18, 2026
Reading recorded: ruling C's "every table" is every table the tenancy anchor governs · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-18T22:42ZStage 8c-0 (PR #24403) executes ruling C. Its at-tier contract review (6256072725, PASS, ①(b) and ③ item 8) asks the seat to record one reading of the ruling before the queue. ⛔ The ruling itself is not changed.
The reading (A). "The verified marker then means zero NULL rows on every table" reads as every table the tenancy anchor governs. Two tables keep their organization-less rows reported and advisory instead of blocking:
sys_sso_provider, and the retiredsys_scim_provider, bothnotNullExemptin the inventory.Why, measured on the head:
sys_sso_providerdeclarestenancy: { enabled: false }. Itsorganization_idis better-auth's "organization scope, when org-scoped SSO is used". A deployment-wide provider bound to no organization is a legitimate row, not "a row whose owner cannot be derived" (「找不到主人的行」).none. So no NULL reader that 8c-1 to 8c-3 retire ever reads either table.sys_scim_provideris retired, and its table is left in place untouched by the 2026-08-24 ruling.The fallback is one line. If the maintainer vetoes A, the change is the
notNullExempt ? 0arm ofheldUnattributable(packages/cli/src/utils/organization-ownership-apply.ts) plus its control pin. It would be a follow-up commit, not a reopening of stage 8c-0. The maintainer's veto window stays open, as for D3.