Repository navigation
[Decision] ADR-0131 D1 on stores already certified (18.0.0-born or ceremonied): an advisory for the voluntary ceremony plus a documented residue, or a shape-generation bump #25134
Description
Ruled: 6328195425 · letter A · 2026-10-21T22:46Z
一句话问题: 已经认证过的库里,大部分组织列现在还允许为空,可 ADR-0131 D1 承诺的是「任何地方都没有可为空的组织列」。这包括两种库:已发布的 18.0.0 新建的库,以及从 v17 升级过来、跑过仪式的库。怎么让这条承诺成真?
背景:
- 7b-0 之后,新建的表组织列从建表起就是 NOT NULL。但已经认证过的库不会自动收紧:
- 18.0.0 新建的库带着出生标记,直接算作已认证,不需要跑仪式;
- 跑过仪式的库,仪式也只收紧它能收紧的表。
- measure(adr-0131 D1): a database 18.0.0 created is certified by its birth marker yet keeps every tenant column nullable, and the ceremony certifies tables it left nullable — D1 is not yet a fact the certificate proves #24919 的测量(6323585273,main
d5d8146012,single、group、isolated三种部署形态结果相同):
| 库 | 可为空的组织列 | 空组织行 |
|---|---|---|
| (a) 18.0.0 新建,在 HEAD 上启动 | 66 张里 65 张 | 0 |
| (b) (a) 再自愿跑一次仪式 | 66 张里 4 张 | 0 |
| (c) v17 升级、跑过仪式 | 66 张里 4 张 | 0 |
| (d) HEAD 全新安装 | 64 张里 1 张(sys_sso_provider,不分租户) |
0 |
- 跑完仪式后剩下的 4 张表,都只需要写进文档:
- 7b-0 之前轮转出来的活动分表,保留期 14 天,到期自然消失;
sys_email_template,D13 会把它退役;sys_notification_subscription,已退役,按裁定原地保留;sys_sso_provider,它不分租户,只是有一列叫organization_id,并不是租户列。
- Turso 远程库:仪式在代码层面没法重建表,所有已有的表都会留成可为空。
- 唯一还会写出空组织的路径是生产环境不配认证密钥的
os serve --no-server。它单独开了一张决策卡;本卡推荐的方案要等那张卡裁定、修复合入后才成立。
Governing text: ADR-0131 D1 ("There is no nullable tenant column anywhere") states the goal but not how an already-certified store reaches it. ADR-0131 D10 ("The migration is a manual, operator-run ceremony — never an automatic boot step", maintainer 2026-09-04 「我建议 18.0 的主要考虑是客户数据变化比较大,而且需要手工执行升级脚本。」) rules out option C. The line ruling keeps ORGANIZATION_OWNERSHIP_SHAPE_GENERATION at 1, so option B would need the maintainer to lift it. Which of these D1 becomes is a decision on ADR text.
前提(带复核命令):
- 出生标记把库认证在第 1 代:
git grep -n "ORGANIZATION_OWNERSHIP_SHAPE_GENERATION" origin/main -- packages/spec/src/system/migration.zod.ts。 - 仪式的 not-null 步骤(
organization-ownership-not-null.ts)只收紧空组织行为 0 的表。 - 今天启动时不会提示运维去跑仪式:
os migrate plan有意不报这一项(driver-sql/src/schema-drift.ts:949、:974)。 - 完整数据和每个选项的代价,见 6323585273 第 1–3 节。
选项:
| 选项 | 做什么 | 客户能感知的后果 | 工作量 |
|---|---|---|---|
| A(推荐) | 仪式继续保持自愿,不强制。加一条提示:启动时打一行 info,或 os migrate plan 列出还可为空的组织列,并写进发布说明。同时在 #24897 里把 D1 改成如实的措辞(见下) |
运维会看到提示,自己挑时间跑仪式;不跑也照常启动。跑完以后只剩上面 4 张表,都已写进文档 | 小:一条提示加测试;D1 的措辞随 #24897 一起改(Tier H,要你批准) |
| B | 把 ORGANIZATION_OWNERSHIP_SHAPE_GENERATION 升到 2 |
所有现有的 18.x 库下次启动都会被拒绝,包括已经跑过仪式的库,必须先跑仪式才能启动。而且跑完照样剩那 4 张表 | 中,但每个客户都要停机升级。这条线一直保持第 1 代,就是为了避免这个 |
| C | 启动时自动收紧可为空的列 | 违反 D10「永远不做自动启动步骤」。在 PostgreSQL 上要锁表全表扫描;碰到一行空组织,要么悄悄跳过,要么拒绝启动(那就等于 B) | 中,风险高:无人值守的破坏性 DDL |
| D | 只改文档,不加提示 | 运维永远不知道该跑仪式,18.0.0 建的库就一直可为空 | 最小 |
A 让 D1 变成的样子(来自测量报告的草稿,随 #24897 落地):
Every object whose tenant column is
organization_idcarries it NOT NULL at the engine from the start (D9), and at the DDL on every table the driver creates and on every table the D10 ceremony has cleared. A database the published 18.0.0 created is certified at birth with its tenant columns nullable;os migrate organization-ownership, which it does not require, tightens them. The ceremony leaves nullable only: an activity shard rotated before the driver created shards NOT NULL (it expires with its retention), a table whose object is retired and left in place by ruling,sys_email_templateuntil its D13 retirement, a table still holding a reported NULL row, and every table on a datasource whose transport cannot rebuild a table (the Turso remote face). A column namedorganization_idon an object with tenancy disabled (sys_sso_provider) is not a tenant column.
业务含义:
- A:老房子不强制翻修,但会贴一张告示,写明翻修的好处和翻修的办法;
- B:所有老房子强制停用,翻修完才能住;
- C:趁住户睡觉偷偷翻修,出了问题没人知道。
os-decision-facets
- ① 项目长远合理性:A 守住 D10 的「手工执行」,又让 D1 的文字和事实一致;B 违背这条线「保持第 1 代」的约束;C 违背 D10。
- ② 实际业务拉动:所有测过的库里空组织行都是 0。可为空的列今天没有读者把空值当真,也就没有客户受害,只是承诺和事实对不上。
- ③ 防 AI 犯错:A 出错,最多是一条提示写错。B 出错,所有客户都起不来。C 出错,就是无人值守地损坏数据。
- ④ 创业阶段不扩散:A 只加一条提示,外加一段文档。
Prior rulings read: nullable,not null,birth marker,shape generation,certified,tighten → 7 hits; ADR-0131 D1, ADR-0109 Decision §4, ADR-0111 D10, ADR-0113 D3 (none decides how a certified store reaches D1); plus ADR-0131 D10; thread: 6323585273, 6297109158; repo: objectstack-ai/objectstack
推荐: A。只看①选 A,②③④ 都不改变结论。回退方案是 D,但那样 D1 的措辞得承认 18.0.0 建的库会一直可为空。
- 置信缺口:Turso 远程库只按代码判断,没有实测;措辞里已经把它算作剩余。
会不会留下迁移债: 不会新增。A 不改表结构,跑不跑仪式由运维决定。
裁后执行:
- 选 A:
- 先等另一张卡(
--no-server写无主数据)的修复合入; - 派一张小卡给 cli/runtime 加提示(启动 info 行或
os migrate plan的说明,二选一,开发测完再定),顺手修掉测量发现的仪式分表说明(organization-ownership-not-null.ts:31、:46在 7b-0 之后已经不对); - docs(adr): ADR-0131 dated notes for the 18.x line — release scope, the D9 lean-embedding exception, D1 at createTable, D2 deployment-wide declared rules (Tier H) #24897 采用上面的 D1 措辞(Tier H,要你批准)。
- 先等另一张卡(
- 选 B:开 Tier H 的实现卡,并发布停机升级说明。
- 选 C:先修订 ADR-0131 D10。
- 选 D:只改 docs(adr): ADR-0131 dated notes for the 18.x line — release scope, the D9 lean-embedding exception, D1 at createTable, D2 deployment-wide declared rules (Tier H) #24897 的措辞。
Blocks: #24897 (the ADR-0131 dated notes), and #24919's closure.
Blocking (director seat, ruling A 6328195425)
Blocked-by: #25133
Activity
objectstack-fleet commentedon Oct 21, 2026
Ruling: batch #382 item 3 · letter A · maintainer 「24932 25133 帮我综合分析工作量和风险;25134 25092 25102 同意」 2026-10-21T22:43Z
Director seat, summon #47,
session_01TQvbE2ENCniJr1ffCzbPc6(os-bill, writing through the relay). Presented in this session's chat as batch #382 item 3, from the card's own analysis; the director's delta re-checked onorigin/mainabc25a3536:ORGANIZATION_OWNERSHIP_SHAPE_GENERATIONis still 1 (packages/spec/src/system/migration.zod.ts:215); the ceremony's NOT NULL step skips rotation shards and the Turso remote face (packages/cli/src/utils/organization-ownership-not-null.ts:25-50);os migrate plandeliberately does not report the ceremony-owned column (ceremonyOwnedNullabilityinpackages/drivers/driver-sql/src/schema-drift.ts). Thread read to its end at this write: no comment on the card.Governing text:ADR-0131 D1 ("There is no nullable tenant column anywhere") and D10 (the migration is a manual, operator-run ceremony, never an automatic boot step; maintainer 2026-09-04 「我建议 18.0 的主要考虑是客户数据变化比较大,而且需要手工执行升级脚本。」); the line ruling that keeps the shape generation at 1.What the ruling binds
os migrate plan, not in a boot line. The execution parameter the card left open ("a boot info line oros migrate plan, decided after the dev measures") is settled here:os migrate plangains one advisory section that lists the tenant columns still nullable and namesos migrate organization-ownershipas the remedy. That section is advisory only: it is not drift, it proposes no op, and it changes no exit code. No boot line is added, because D10 makes the ceremony an operator-run migration, so its prompt belongs on the operator's migration tool, and a boot line would repeat on every node at every start where nobody reads it. The same advisory goes into the 18.0 upgrade notes.organization-ownership-not-null.ts(:31,:46, written before 7b-0) are corrected in the same small PR.--no-serverownerless-write path): the advisory's "the ceremony leaves nullable only …" claim holds only once that path is closed.Effort of the chosen letter (the card's column): small — one advisory plus its test; D1's wording rides #24897. Confidence gap carried: the Turso remote face is read from code only, not measured; the wording already counts it as a residue.
State:
needs-user-decision→pm:blocked,Blocked-by: #25133in the body. When #25133's fix has merged, the epic seat dispatches one smalldomain:clicard for items 2 and 4, and #24897 takes item 3. Letters B, C and D are not taken: B refuses every existing 18.x store at its next boot and still leaves the four documented tables; C breaks D10; D leaves operators unaware the ceremony exists.Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Claim: epic seat round · 2026-10-22T05:22Z · executing the maintainer's ruling 6328195425 (letter A, 「25134 … 同意」); its blocker #25133 landed as
ab488d9b7dSession:
session_01Y5583bs2RDd53MH6kM1ca3Account:
os-steve(the card's assignee)Branch:
claude/issue-25134-migrate-plan-nullable-advisoryWorktree: the cloud session's own checkout (
mode:cloud)Domain:
domain:engineSeat:
epic:#15194File surface:
packages/cli/src/**: theos migrate planoutput gains one advisory section. It lists the tenant columns that are still nullable and namesos migrate organization-ownershipas the remedy. Advisory only: it is not drift, it proposes no op, and it changes no exit code.packages/cli/src/utils/organization-ownership-not-null.ts: the stale shard sentences (:31,:46, written before 7b-0) are corrected, including the "unsupported" report of a shard that is already NOT NULL.content/docs/deployment/cli.mdx(theos migrate plansection) andcontent/docs/upgrading.mdx(the 18.0 upgrade notes line)..changeset/*.md.content/docs/releases/**.docs/adr/**(docs(adr): ADR-0131 18.x notes, review precision (B1–B3, B6) and the #25155 / #25179 citations (Tier H) #25154).ORGANIZATION_OWNERSHIP_SHAPE_GENERATIONchange.Container & model:
S,mode:cloud,model: TIER_DEFAULT. The review is at the tier the diff's declared face picks.Clause-②: no — expected: an advisory section in a read-only plan output; no outcome, exit code, op or refusal moves; the dev re-declares from the diff
Responsibility: an 18.0.0-born or ceremonied store is certified while tenant columns stay nullable, and nothing tells the operator the voluntary ceremony tightens them (6323585273) | none | reached by every operator reading
os migrate planon such a storeThread-read: 6328195425
Serial constraints cleared: [Decision] A production os serve that composes no organization object (no auth secret, or no auth) still writes ownerless rows under --no-server: refuse that boot too, or refuse such writes in the engine everywhere #25133's fix (fix(cli)!: refuse a production os serve that composes no organization object, --server or not #25155) merged as
ab488d9b7d. docs(adr): ADR-0131 18.x notes, review precision (B1–B3, B6) and the #25155 / #25179 citations (Tier H) #25154 waits on this PR's merge commit for its last citation.objectstack-fleet commentedon Oct 22, 2026
os-dev-report
{ "issue": 25134, "status": "done", "branch": "claude/issue-25134-migrate-plan-nullable-advisory", "pr": "https://github.com/objectstack-ai/objectstack/pull/25179", "session": "session_01RXxXFcq659RGJE6SsXAc3u", "premise_still_valid": true, "summary": "os migrate plan gains one advisory section (packages/cli/src/utils/nullable-tenant-columns.ts) listing every tenant column still nullable, read from the same per-table tenant field the differ exempts as ceremony-owned, with os migrate organization-ownership as the remedy. It is not drift, proposes no op and moves no exit code; under --json it is the separate nullableTenantColumns field. Residue is marked apart (ceremony: leaves + reason: pre-7b-0 rotation shard, sys_email_template's mirror-deletion fate, an inventory notNullExempt, or a driftApplyLimit on the Turso remote face), so a ceremonied store is never sent back to the ceremony; the opt-out sys_sso_provider and undeclared tables are never listed. Measured on fixture-18.0.0: 63 listed (61 tightens = the ceremony's measured applied 61, 2 leaves); after the ceremony only the 2 residue, all leaves; a fresh store lists nothing. The ceremony's not-null step corrects the stale pre-7b-0 shard sentences and reports a shard the driver created NOT NULL as already-not-null (new alreadyNotNull summary count) instead of unsupported. Docs: cli.mdx os migrate plan subsection, upgrading.mdx 18.0 paragraph. Clause-② re-declared from the diff as yes (widening): the claim's expected 'no' did not hold, since a --json field and an outcome value are added; Metadata-protocol: none, patch.", "tests": "head 8ad60d9a. Changed cli files: vitest 4 files / 30 tests pass (nullable-tenant-columns.test.ts, organization-ownership-not-null.test.ts, organization-ownership-apply.generation-1.test.ts, plan.nullable-tenant-columns.integration.test.ts [spawned, integration tier]). cli unit tier (--project unit): first run 321/322 files, 4754/4756 tests; the 2 red were plan.apply-limit.test.ts pinning the --json key set this PR widens, lists updated, re-run 19/19 with plan.journal-mode-note.test.ts. Upgrade harness whole file (upgrade-18x-harness.dogfood.test.ts, 18.0 + 18.0.0 legs): 15/15, lock VERDICT command-exit 0. cli typecheck exit 0; dogfood typecheck exit 0 (the first run aborted on the 4 GB heap; re-run at 6144 MB); eslint on the 10 changed .ts files exit 0. Ablations through scripts/ablation-replace.mjs (CLI pins run from source; no dist), each restored to blob == HEAD: A1 nullability filter -> 5 red; A2 residue marking -> 3 red; A3 advisory counted into total -> red at the total assertion; A4 exitCode=1 when the section prints -> red at the human exit assertion; A5 --json field dropped -> red at the fresh-store field; A6 shard isNotNull check removed -> red at the already-not-null pin. The first A1/A4/A5 attempts were refused by the tool (counts could not move) and measured nothing; re-run with moving anchors. Harness pins were not ablated (they run dist). R1: pnpm dev:crm -- --fresh ready, health 200; production os serve of examples/app-crm with a secret ready, health 200; os migrate plan on that fresh store: nullableTenantColumns read, columns [], exit 0. R4: walled production os serve (group, invite-only, owner email, secret) with @objectstack/organizations composed, ready, health 200; os migrate plan there: columns [], exit 0.", "gates": "node scripts/pm/dispatch-gates.mjs --commands: 100 families, exit codes captured before any pipe; --ran: 100 accounted, 99 run, 1 NOT MEASURED. 99 exit 0, two after a re-run: check-plugin-teardown-shape --self-test (exit 3 on the shallow clone, then 0 after fetching its pinned commit) and check:skill-examples (exit 3 with client-react unbuilt, then 0 after building it). NOT MEASURED: check:dual-build-cjs-loads (exit 3, needs a whole-monorepo build; declared to CI). Ratchet re-run on final head 8ad60d9a, all exit 0: check:type-check-coverage, check:type-check-debt, check:cross-package-test-inputs, check:test-source-alias, check:tier-file-adoption, check:durability-log-level, check-changeset-no-major, check-adr-0087-registration, check:nul-bytes, check:doc-authoring. check-governed-merges --branch origin/claude/issue-25134-migrate-plan-nullable-advisory: exit 0, NOT governed, 0 of 15 paths, +926/-33. PR check-runs: in_progress at this report (PR opened at 8ad60d9a).", "dogfood_battery": "n/a: no ! in the PR title and no (narrowing) arm. The upgrade harness file was run on its own: 15/15.", "mcp_calls": "6 reads: issue_read x4 (#25134 get, #25134 get_comments, #24919 get_comments, #15194 get), search_issues x1, search_pull_requests x1. No write tool.", "api_writes": "2 relay strokes: pr_create (POST /repos/objectstack-ai/objectstack/pulls plus POST .../issues/25179/assignees), and this comment (post-stamped --comment=25134)", "open_questions": [], "out_of_scope_findings": [ "carrier: none · noted, not filed: os migrate plan run without OS_AUTH_SECRET composes no auth family (the same rule as os serve), so the advisory cannot see that family's tenant tables (52 listed on fixture-18.0.0 against 63 with a secret), and the unmanaged-table section lists them as declared by nothing. This is the whole plan's existing composition rule, not the advisory's; it is in the PR's Acceptance notes." ] }Generated by Claude Code
objectstack-fleet commentedon Oct 22, 2026
Landed · epic PM
session_01Y5583bs2RDd53MH6kM1ca3(os-steve) · 2026-10-22T07:30Z. PR #25179 merged intomainas6251f59051. It executes ruling 6328195425 (letter A, items 2 and 4) and closes this card.os migrate planhas a new advisory section. It lists every tenant column still nullable, namesos migrate organization-ownershipas the remedy, and marks each column as one the ceremony tightens or leaves, with the reason.--jsonit is the separate fieldnullableTenantColumns.not-nullstep now reports a rotation shard that the driver created NOT NULL asalready-not-null(summaryalreadyNotNull), instead ofunsupported. The stale pre-7b-0 shard sentences are corrected.cli.mdxos migrate plansubsection and the 18.0 paragraph inupgrading.mdx.tightens, which equals the ceremony's applied 61, and 2leaves, the pre-7b-0 shard andsys_email_template.Clause-②: yes (widening): one--jsonkey and one outcome value are added. On that declaration face, theTIER_DEFAULTreview passed (6334904919).