Skip to content

[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

Closed

Description

@objectstack-fleet

Ruled: 6328195425 · letter A · 2026-10-21T22:46Z

一句话问题: 已经认证过的库里,大部分组织列现在还允许为空,可 ADR-0131 D1 承诺的是「任何地方都没有可为空的组织列」。这包括两种库:已发布的 18.0.0 新建的库,以及从 v17 升级过来、跑过仪式的库。怎么让这条承诺成真?

背景:

库 可为空的组织列 空组织行
(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_id carries 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_template until 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 named organization_id on 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 不改表结构,跑不跑仪式由运维决定。

裁后执行:

Blocks: #24897 (the ADR-0131 dated notes), and #24919's closure.

Blocking (director seat, ruling A 6328195425)

Blocked-by: #25133

Activity

  1. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    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 on origin/main abc25a3536: ORGANIZATION_OWNERSHIP_SHAPE_GENERATION is 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 plan deliberately does not report the ceremony-owned column (ceremonyOwnedNullability in packages/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

    1. The ceremony stays voluntary. The shape generation stays at 1; no store is refused at boot for nullable tenant columns; no automatic tightening runs at boot (D10 stands).
    2. The advisory lives in os migrate plan, not in a boot line. The execution parameter the card left open ("a boot info line or os migrate plan, decided after the dev measures") is settled here: os migrate plan gains one advisory section that lists the tenant columns still nullable and names os migrate organization-ownership as 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.
    3. D1's wording becomes the card's draft (the paragraph under "A 让 D1 变成的样子"), landing with 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 (Tier H, the maintainer's approval).
    4. The stale shard sentences in organization-ownership-not-null.ts (:31, :46, written before 7b-0) are corrected in the same small PR.
    5. Order. This card is blocked by [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 (the --no-server ownerless-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: #25133 in the body. When #25133's fix has merged, the epic seat dispatches one small domain:cli card 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

  2. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: epic seat round · 2026-10-22T05:22Z · executing the maintainer's ruling 6328195425 (letter A, 「25134 … 同意」); its blocker #25133 landed as ab488d9b7d
    Session: session_01Y5583bs2RDd53MH6kM1ca3
    Account: os-steve (the card's assignee)
    Branch: claude/issue-25134-migrate-plan-nullable-advisory
    Worktree: the cloud session's own checkout (mode:cloud)
    Domain: domain:engine
    Seat: epic:#15194
    File surface:

  3. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    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

  4. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-22T07:30Z. PR #25179 merged into main as 6251f59051. It executes ruling 6328195425 (letter A, items 2 and 4) and closes this card.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions