Skip to content

docs(adr): ADR-0131 §1.2(3) — the precondition is reachable and reported at boot, not a refused boot - #17544

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-17467-adr-0131-refused-boot-premise
Sep 11, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-17467-adr-0131-refused-boot-premise

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes #17467

Clause-②: no

ADR-0131 §1.2 item 3 called the measured NULL-row leak's precondition "today a refused boot",
citing ADR-0093 D5. Nothing refuses it. This corrects that one item in place and says, in one
clause, what the correction does to §1.2's argument. It decides nothing about whether the boot
should be refused — that fork is the maintainer's and #17010 deliberately left it open.

Item 3 — before / after

Before (docs/adr/0131-total-organization-ownership-no-null-organization-id.md:132-133):

3. The measured leak's precondition — many organizations with Layer 0 inert — is today a refused boot
   ([ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) D5, cloud#1020, cloud#1664).

After (:132-139):

3. The measured leak's precondition — many organizations with Layer 0 inert — is **reachable today**,
   not a refused boot: a deployment that never *requests* a walled posture and merely *holds* more
   than one `sys_organization` row under `single` boots and serves. Since #17010 (PR #17460)
   `TenancyService` takes a `count(sys_organization)` census and **reports** that state at `error` at
   boot — naming the posture the deployment declared, the count it holds and the two remedies — and
   does not refuse it; [ADR-0093](./0093-tenancy-mode-and-membership-lifecycle.md) D5 refuses the
   different case of a *requested*-but-absent wall (cloud#1020, cloud#1664). The discount this item
   gives the leak therefore rests on a reported state, not a refused one.

The last sentence is the part that keeps §1.2 honest. Item 3 is the reason the measured leak is
discounted — the precondition cannot happen, because that boot is refused. Deleting the item
would have left the discount with a silent gap; correcting the premise and naming what the discount
now rests on keeps the argument readable in the direction the record intends.

Everything else in the file is byte-identical: one hunk, 8 insertions / 2 deletions.

The three readings this sentence is built on

1. The behaviour on main todaypackages/plugins/plugin-auth/src/tenancy-service.ts
at e758131b (PR #17460, merged 2026-09-10T16:37Z), with line numbers:

  • :316const counted = await engine.count('sys_organization', {}, { context: SYSTEM_CTX });
    (the census itself, inside probeOrganizationCount, documented at :304 as "Never throws").
  • :343if (postureEnforcesWall(census.posture)) return null; — a walled posture reports nothing.
  • :348-364 — the report text, which itself states 'nothing refused this boot and THE DEPLOYMENT WILL KEEP LOOKING HEALTHY' and spells the two remedies ("TWO WAYS OUT, and this deployment has to
    pick one: (1) DECLARE A WALLED POSTURE … or (2) HOLD ONE ORGANIZATION …").
  • :401-402if (logger?.error) logger.error(message, meta); / else logger?.warn(message, meta);
    — the error channel, with the warn fallback for a sink with no error.
  • :491-500 — the call site on TenancyService.defaultOrgId(), reached at boot through the
    kernel:ready membership backfill; the census runs once per process and the method still
    return resolved;.
  • Nothing refuses. grep -n 'throw' packages/plugins/plugin-auth/src/tenancy-service.ts returns
    four hits and all four are in comments or docblocks (:279, :304, :382, :404) — there is no
    throw statement and no non-zero exit on the census path. The file's own docblock at :69 says it
    outright: "Boot PROCEEDS. This census only REPORTS".

2. ADR-0093 D5's own text, so the contrast in item 3 is a reading and not an assertion —
docs/adr/0093-tenancy-mode-and-membership-lifecycle.md:292-296:

At boot, when tenancy.requested === true and the organizations package fails to load (missing, or
its init() throws): — Default: refuse to boot. Exit non-zero with an actionable error naming
the package, the flag, and the two remedies (install the package / unset the flag).

and its summary at :50-51: "D5 — The degraded middle state fails fast:
OS_MULTI_ORG_ENABLED=true without a working @objectstack/organizations refuses to boot."
That precondition is a wall the deployment requested and cannot have. It does not reach a
deployment that requested no wall at all.

3. The two occurrences of the phrase, and :558 untouched.

Before the edit, grep -n 'refused boot' on the file returned exactly two:

132:3. The measured leak's precondition — many organizations with Layer 0 inert — is today a refused boot
558:whether the wall is enforced differs. Degraded tenancy stays a refused boot. `single` is never a

After the edit it returns two again, and the D11 line is byte-identical — only its line number moved
(:558 to :564, because item 3 grew by six lines):

133:   not a refused boot: a deployment that never *requests* a walled posture and merely *holds* more
564:whether the wall is enforced differs. Degraded tenancy stays a refused boot. `single` is never a

Note the shape of the new :133: the phrase is now carried by a negation inside the corrected
item, so a future grep 'refused boot' on this file still finds two lines, one asserting and one
denying, rather than one. The D11 sentence at :564 is TRUE and was not edited — proved by
od -c on that line against git show origin/main:docs/adr/0131-…md | sed -n '558p': identical
bytes, and the diff carries a single hunk that does not reach it.

The one judgement, on the four axes

The only judgement here is how the corrected item states the discount — say plainly that the
discount now rests on a reported state (chosen), versus deleting item 3, versus correcting only the
premise and leaving the conclusion unqualified. On 实际业务需求 the pull is measured, not
speculative: #17010 found a real deployment (objectstack-ai/ats) in exactly this state, and this
item is the sentence a reader lands on when asking whether the leak is reachable — a silent gap
there is read as "settled", which is the failure the card names. On 项目长远合理性, §1.2 is a
findings section whose items each cite their evidence inline (#10103, #12699, cloud#1239), so
citing #17010 / PR #17460 in place is the record maintaining itself contract-first, whereas
deleting the item would leave a decision resting on a premise nobody can find. On 防 AI 写代码
犯错
, the corrected sentence is the strict form: it states what the runtime actually does
(error-level report), which is the behaviour a later agent can verify against tenancy-service.ts,
instead of a tolerant "may be refused" that lets a reader assume enforcement that does not exist —
declared must equal enforced. On 创业阶段不扩散需求, the edit stays at one item and adds no
recommendation, no amendment section and no new decision: the refuse-or-report fork is left exactly
as open as #17010 left it. The axes agree; there is no trade-off to hand up.

Verification

Gate families derived from the FINAL diff in the worktree, not recalled:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 8f7761df
— 18 commands, change set = 1 path (docs/adr/0131-…md), merge base edaf3b2f7, three-dot.
Every exit code captured BEFORE any pipe (cmd > log 2>&1; EXIT=$?).

# command exit its own verdict line
1 node scripts/check-adr-links.mjs 0 check-adr-links: 680 relative link destination(s) under docs/adr/ resolve
2 node scripts/check-adr-links.mjs --self-test 0 check-adr-links --self-test: discrimination, census, ADR-0046 pin and baseline staleness all verified
3 node scripts/check-adr-symbol-anchors.mjs 0 check-adr-symbol-anchors: 2073 anchors across 139 records resolve … 0 line anchors survive.
4 node scripts/check-adr-symbol-anchors.mjs --self-test 0 check-adr-symbol-anchors --self-test: every finding class provoked, healthy anchors silent, population live, wiring pinned (2073 live anchors)
5 node scripts/check-ci-filter-parity.mjs 0 OK: all 174 declared cross-package glob(s) (125 unique) are covered by 'core' or 'crosspkg' …
6 node scripts/check-closing-keyword-parity.mjs 0 check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords … 5 file(s) … all registered).
7 node scripts/check-closing-keyword-parity.mjs --self-test 0 check-closing-keyword-parity --self-test: 24 assertions, 5 mutations of the shipped parsers each driven to red.
8 node scripts/check-comment-mask-corpus.mjs 0 comment-mask corpus sweep: 6587 files, 0 disagree, 0 unparseable, 60.7s
9 node scripts/report-test-timings.mjs --self-test 0 report-test-timings: self-test OK (61 cases across 4 batteries …)
10 pnpm --filter @objectstack/lint run check:doc-formula-expressions 0 (see note) check:doc-formula-expressions: 22 record-scoped formula example(s) across 438 files / 1375 TS blocks judged clean by @objectstack/formula.
11 pnpm check:adr-anchors 0 check-adr-anchors: OK (53 anchored file(s) … 34760 citation(s) across 4364 file(s) resolve; 1019 decision-letter citation(s) … name a decision the record makes).
12 pnpm check:cross-package-test-inputs 0 OK: 28 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob
13 pnpm check:doc-authoring 0 doc authoring guard: 15039 customer-facing string(s) across 939 spec sources clean — no internal issue-id references
14 pnpm check:driver-memory-census 0 check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live …
15 pnpm check:nul-bytes 0 check-nul-bytes: OK (scanned 8321 text file(s) … no raw ASCII control bytes).
16 pnpm check:pm-governed-merges 0 check-governed-merges --self-test: 317 assertions …
17 pnpm check:refd-timer-probe 0 OK check-refd-timer-probe: 6582 source file(s) swept …
18 pnpm check:watch-hint-literal 0 check-watch-hint-literal: 69 declaration(s) across 4 rostered name(s) … no unrostered spelling of the idiom in the tree.

Note on #10. Its first run exited 3, which is that gate's PREREQUISITE NOT MET code and
explicitly not a finding — its own words: "Nothing was measured: this gate exited before running
a single check, so this result says NOTHING about what it gates." @objectstack/formula and
@objectstack/lint were unbuilt in a fresh worktree. Built through the shared verify lock
(bash scripts/pm/os-verify-lock.sh -c 'pnpm exec turbo run build --concurrency=2 --filter=@objectstack/formula --filter=@objectstack/lint', VERDICT command-exit 0), then re-run:
exit 0 with the verdict line above, plus its self-test (58 cases), its spec-TSDoc leg (9 examples)
and its field-level leg (14 predicates judged, 6 skipped as undeterminable).

Reconciliationnode scripts/pm/dispatch-gates.mjs --ran … --repo objectstack-ai/objectstack,
each line recorded as command :: exit N:

dispatch-gates --ran: 18 derived famil(ies) accounted for — 18 run, 0 NOT-MEASURED
(a DERIVED zero — all 18 recorded an exit code and none of them is 3).

Path facenode scripts/pm/check-governed-merges.mjs --branch claude/issue-17467-adr-0131-refused-boot-premise,
exit 3 (this gate's GOVERNED verdict code, not a failure):

governed-surface predicate: 1 of 1 path(s) hit the register (5 surfaces, repo-agnostic).
  GOVERNED — a human merge is the review record for this PR (#9495 regime).
      docs/adr/** ×1 — architecture decision records

This PR stays DRAFT. No seat flips it ready, enqueues it, or arms auto-merge
(AGENTS.md Prime Directive #14). The maintainer merges.

Model tiernode scripts/pm/dispatch-gates.mjs --tier docs/adr/0131-…md:
"Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s)".

Control characters — beyond check:nul-bytes, a direct scan of the edited file:
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' returned no matches (exit 1).

Changeset: none, and skip-changeset applied. Nothing published moves — the diff is one file
under docs/adr/**, which is on the fast-track non-published list; no package files[] ships it,
no source, no type, no wire shape. Not derived by hand: the gate derivation above lists 14 families
as "apply once this card's changeset exists", i.e. none of them is owed by a diff with no package
change.

Not run locally, on purpose: the repo-wide scans (pnpm lint and the rest of the lint
workflow's farm) are CI's run, not this PR's local debt; and the derivation names, outside its 18,
49 artifact-roster families, 11 declared-wide families, 14 pending-changeset families and 1
path-scheduled CI job — none of whose rosters sits in a directory this diff touches, per its own
line: "None of their rosters sits in a directory your paths are in".

Acceptance notes

  • noted, not filed: the dispatch's register reading — "No 'Amendments' section exists in this
    repo's ADRs (measured: 0 files)" — is exact for the literal plural heading ## Amendments
    (grep -rn '^## Amendments' docs/adr/ = 0), but 16 ADR files do carry a singular
    ## Amendment (date, #card): … section (grep -rln '^#\+ .*[Aa]mendment' docs/adr/ = 16), and
    docs(adr): ADR-0094 D2 under-describes its own shipped code — the recovery doors project too, and D3 is no longer the only healing path #15244 — one of the "same class" precedents the card names — landed exactly that way in ADR-0094.
    Not used here for three reasons, recorded so the choice reads as decided: that convention marks a
    decision changing (a D-item reversed, a gate retired), whereas §1.2 is a findings section whose
    items are dated by the evidence they cite; an end-of-file section would breach this card's
    declared file surface (§1.2 item 3 only); and ADR-0131 carries no amendment note of its own to
    match. Carrier if it is ever wanted: whoever next amends an ADR-0131 decision.
  • noted, not filed: the corrected item makes the phrase "refused boot" appear on three lines
    of this file rather than two (:133 denying, :564 asserting, and the card's fence table now
    under-counts by one). Nobody is harmed — the card's real point, that a blind sed over the phrase
    would break :564, is now more true, not less — but a future editor reading the card's table
    should expect three. Carrier: the next editor of ADR-0131 §1.2 or D11.
  • noted, not filed: packages/plugins/plugin-auth/src/tenancy-service.ts:56-58 quotes the very
    sentence this PR rewrites ("ADR-0131 §1.2(3) states … 「is today a refused boot」. It is not.").
    That docblock is still correct after this merge — it quotes the OLD text and says it is wrong,
    which remains a true statement about what the record used to say — but it is now describing a
    sentence the record no longer carries. Deliberately not touched: it is outside this card's
    declared file surface, it is a code comment (not a contract), and it is neither a reproducible
    defect, a contract violation, nor a metadata-authoring trap, so it is not filing-eligible under
    Prime Directive chore: version packages #10. Carrier: whoever next edits that docblock, e.g. if the maintainer settles the
    refuse-or-report fork.

维护者速读(草稿)

改了什么 — 只改 docs/adr/0131-…md §1.2 第 3 条一项,原地重写,一个 hunk、8 增 2 删。原文说
「这个前提条件今天是一次被拒绝的启动(refused boot)」并引 ADR-0093 D5;改后说的是实测事实:该前提
今天可达,不是被拒绝的启动——一个从未请求围墙姿态、只是在 single持有多于一行
sys_organization 的部署照常启动并提供服务;自 #17010(PR #17460)起 TenancyService 做一次
count(sys_organization) 普查,在启动时以 error报告该状态(点名声明的姿态、持有的行数、
两条补救路径),但不拒绝它;D5 拒的是另一件事——请求了却拿不到的围墙。末尾补一句:第 3 条给那个
泄漏打的折扣,因此建立在一个「已报告」的状态上,而不是一个「已拒绝」的状态上。

为什么改 — 这句话不是装饰,它是 §1.2 用来给已实测的 NULL 行泄漏「打折」的那一条理由(前提不可能
发生,因为那种启动会被拒绝)。前提是假的,折扣就没有挣来,读者会对泄漏的可达性得出与记录本意相反的
结论。这是决策记录里的一个过期前提,不是错别字。

风险与代价(含回滚) — 风险接近零:纯文档,无代码路径、无 spec、无发布物移动,因此不带 changeset
(打了 skip-changeset)。18 个由最终 diff 推导出的门禁族全绿(第 10 个首轮 exit 3 是「前置条件未满足」
而非发现,补建 @objectstack/formula / @objectstack/lint 后 exit 0)。⛔ :564 的 D11
「Degraded tenancy stays a refused boot.」是的,一字节未动(已用 od -corigin/main 逐字节
比对)。回滚 = revert 这一个 commit,不牵连任何东西。

席位意见 — (留空,由 skills 席位定稿)

你要做的 — ① 这是受管面(docs/adr/**,CODEOWNERS 路由),PR 保持 draft,由您手工合并;
任何 AI 席位都不会翻 ready、不入合并队列、不挂 auto-merge。② 请特别确认一句话的边界:本 PR 完全没有
碰「这个启动到底该不该被拒绝」这个问题
#17010 是刻意把它留开的,PR #17460 也只做了报告不做拒绝;
本次改动只是把记录里那句已经不成立的事实陈述改成实测事实,那个岔路口仍然完整地摆在您面前,依然是您的。
③ 如果您决定「应该拒绝」,那需要的是一条新的决策(新 ADR 或在旧 ADR 上加修订状态行),不是再改这一句。


Generated by Claude Code

…ted, not a refused boot

§1.2 item 3 called the measured leak's precondition — many organizations with
Layer 0 inert — "today a refused boot", citing ADR-0093 D5. Nothing refuses it.
D5 refuses a REQUESTED-but-absent wall; a deployment that never requests a walled
posture and merely holds more than one `sys_organization` row under `single`
boots and serves. Since #17010 (PR #17460) `TenancyService` takes a
`count(sys_organization)` census and reports that state at `error` at boot,
naming the declared posture, the count and the two remedies — it does not refuse
it.

Item 3 is the reason §1.2 discounts the measured NULL-row leak, so the corrected
item says in one clause what the correction does to that argument: the discount
now rests on a reported state, not a refused one.

D11 (`Degraded tenancy stays a refused boot.`) is true and is untouched — byte
identical. Whether the boot SHOULD be refused stays the maintainer's open fork;
nothing here decides it.

Claude-Session: https://claude.ai/code/session_01YKEjmbYNvYWJvWGSWx26zK
Co-authored-by: Claude <noreply@anthropic.com>
@os-litant os-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 10, 2026 — with Claude
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 10, 2026

Copy link
Copy Markdown
Collaborator Author

维护者速读

改了什么 —— 只改 docs/adr/0131-total-organization-ownership-no-null-organization-id.md §1.2 第 3 条,原地重写(一个 hunk,8 增 2 删)。原文说这个前提「今天是一次被拒绝的启动」并引 ADR-0093 D5;改后写实测事实:该前提今天可达——从未请求围墙姿态、只是在 single持有多于一行 sys_organization 的部署照常启动并提供服务;自 #17010(PR #17460)起 TenancyService 做一次 count(sys_organization) 普查,在启动时以 error报告该状态(点名声明的姿态、持有的行数、两条补救),但不拒绝;D5 拒的是另一件事——请求了却拿不到的围墙。末尾补一句:第 3 条给泄漏打的折扣,建立在「已报告」而非「已拒绝」的状态上。

为什么改 —— 这句话是 §1.2 给已实测的 NULL 行泄漏打折的那条理由(「前提不可能发生,因为那种启动会被拒绝」);前提是假的,折扣没有挣来,读者会对泄漏的可达性得出与记录本意相反的结论。这是决策记录里的过期前提,不是错别字。

风险与代价(含回滚) —— 纯文档,无代码路径、无 changeset。:564 的 D11「Degraded tenancy stays a refused boot.」是真的,本席在 head 8f7761df 上与 main 逐字节比对未动;ADR 的三个门禁(links / symbol-anchors / anchors)本席在 head 上跑过全绿;CI 22 过 / 11 跳 / 1 在跑,无失败。回滚 = revert 这一个 commit。

席位意见 —— 通过。受管面(docs/adr/**):本席不翻 ready、不入队、不批准。本 PR 没有碰「这个启动该不该被拒绝」——#17010 刻意留开、PR #17460 只报告不拒绝,那个岔路口仍然是您的;若您裁「应该拒绝」,那是一条新决策(新 ADR 或修订状态行),不是再改这一句。

你要做的 —— 一个动作:人工合并本 draft PR。

skills 席,session session_01YKEjmbYNvYWJvWGSWx26zK,2026-09-10T20:47Z。


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 11, 2026 06:30
@os-zhuang
os-zhuang enabled auto-merge September 11, 2026 06:30
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 11, 2026
Merged via the queue into main with commit e03cb88 Sep 11, 2026
40 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-17467-adr-0131-refused-boot-premise branch September 11, 2026 07:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation needs-user-decision size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs(adr): ADR-0131 §1.2(3) calls a precondition "today a refused boot" — nothing refuses it, and #17010 measured the silent boot it describes

3 participants