docs(adr): ADR-0131 §1.2(3) — the precondition is reachable and reported at boot, not a refused boot - #17544
Conversation
…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>
维护者速读改了什么 —— 只改 为什么改 —— 这句话是 §1.2 给已实测的 NULL 行泄漏打折的那条理由(「前提不可能发生,因为那种启动会被拒绝」);前提是假的,折扣没有挣来,读者会对泄漏的可达性得出与记录本意相反的结论。这是决策记录里的过期前提,不是错别字。 风险与代价(含回滚) —— 纯文档,无代码路径、无 changeset。 席位意见 —— 通过。受管面( 你要做的 —— 一个动作:人工合并本 draft PR。 skills 席,session Generated by Claude Code |
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):After (
:132-139):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
maintoday —packages/plugins/plugin-auth/src/tenancy-service.tsat
e758131b(PR #17460, merged 2026-09-10T16:37Z), with line numbers::316—const counted = await engine.count('sys_organization', {}, { context: SYSTEM_CTX });(the census itself, inside
probeOrganizationCount, documented at:304as "Never throws").:343—if (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 topick one: (1) DECLARE A WALLED POSTURE … or (2) HOLD ONE ORGANIZATION …").
:401-402—if (logger?.error) logger.error(message, meta);/else logger?.warn(message, meta);— the
errorchannel, with thewarnfallback for a sink with noerror.:491-500— the call site onTenancyService.defaultOrgId(), reached at boot through thekernel:readymembership backfill; the census runs once per process and the method stillreturn resolved;.grep -n 'throw' packages/plugins/plugin-auth/src/tenancy-service.tsreturnsfour hits and all four are in comments or docblocks (
:279,:304,:382,:404) — there is nothrowstatement and no non-zero exit on the census path. The file's own docblock at:69says itoutright: "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:and its summary at
:50-51: "D5 — The degraded middle state fails fast:OS_MULTI_ORG_ENABLED=truewithout a working@objectstack/organizationsrefuses 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
:558untouched.Before the edit,
grep -n 'refused boot'on the file returned exactly two:After the edit it returns two again, and the D11 line is byte-identical — only its line number moved
(
:558to:564, because item 3 grew by six lines):Note the shape of the new
:133: the phrase is now carried by a negation inside the correcteditem, so a future
grep 'refused boot'on this file still finds two lines, one asserting and onedenying, rather than one. The D11 sentence at
:564is TRUE and was not edited — proved byod -con that line againstgit show origin/main:docs/adr/0131-…md | sed -n '558p': identicalbytes, 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 thisitem 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), sociting
#17010/ PR #17460 in place is the record maintaining itself contract-first, whereasdeleting 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 againsttenancy-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/objectstackat8f7761df— 18 commands, change set = 1 path (
docs/adr/0131-…md), merge baseedaf3b2f7, three-dot.Every exit code captured BEFORE any pipe (
cmd > log 2>&1; EXIT=$?).node scripts/check-adr-links.mjscheck-adr-links: 680 relative link destination(s) under docs/adr/ resolvenode scripts/check-adr-links.mjs --self-testcheck-adr-links --self-test: discrimination, census, ADR-0046 pin and baseline staleness all verifiednode scripts/check-adr-symbol-anchors.mjscheck-adr-symbol-anchors: 2073 anchors across 139 records resolve … 0 line anchors survive.node scripts/check-adr-symbol-anchors.mjs --self-testcheck-adr-symbol-anchors --self-test: every finding class provoked, healthy anchors silent, population live, wiring pinned (2073 live anchors)node scripts/check-ci-filter-parity.mjsOK: all 174 declared cross-package glob(s) (125 unique) are covered by 'core' or 'crosspkg' …node scripts/check-closing-keyword-parity.mjscheck-closing-keyword-parity: OK (3 parsers agree on all 9 keywords … 5 file(s) … all registered).node scripts/check-closing-keyword-parity.mjs --self-testcheck-closing-keyword-parity --self-test: 24 assertions, 5 mutations of the shipped parsers each driven to red.node scripts/check-comment-mask-corpus.mjscomment-mask corpus sweep: 6587 files, 0 disagree, 0 unparseable, 60.7snode scripts/report-test-timings.mjs --self-testreport-test-timings: self-test OK (61 cases across 4 batteries …)pnpm --filter @objectstack/lint run check:doc-formula-expressionscheck:doc-formula-expressions: 22 record-scoped formula example(s) across 438 files / 1375 TS blocks judged clean by @objectstack/formula.pnpm check:adr-anchorscheck-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).pnpm check:cross-package-test-inputsOK: 28 package(s) read outside themselves, all declared, and turbo.json hashes every declared globpnpm check:doc-authoringdoc authoring guard: 15039 customer-facing string(s) across 939 spec sources clean — no internal issue-id referencespnpm check:driver-memory-censuscheck-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live …pnpm check:nul-bytescheck-nul-bytes: OK (scanned 8321 text file(s) … no raw ASCII control bytes).pnpm check:pm-governed-mergescheck-governed-merges --self-test: 317 assertions …pnpm check:refd-timer-probeOK check-refd-timer-probe: 6582 source file(s) swept …pnpm check:watch-hint-literalcheck-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 METcode andexplicitly 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/formulaand@objectstack/lintwere 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).
Reconciliation —
node scripts/pm/dispatch-gates.mjs --ran … --repo objectstack-ai/objectstack,each line recorded as
command :: exit N:Path face —
node 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):
⇒ This PR stays DRAFT. No seat flips it ready, enqueues it, or arms auto-merge
(AGENTS.md Prime Directive #14). The maintainer merges.
Model tier —
node 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-changesetapplied. Nothing published moves — the diff is one fileunder
docs/adr/**, which is on the fast-track non-published list; no packagefiles[]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 lintand the rest of the lintworkflow'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 thisrepo'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), anddocs(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 linesof this file rather than two (
:133denying,:564asserting, and the card's fence table nowunder-counts by one). Nobody is harmed — the card's real point, that a blind
sedover the phrasewould break
:564, is now more true, not less — but a future editor reading the card's tableshould 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-58quotes the verysentence 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 -c与origin/main逐字节比对)。回滚 = revert 这一个 commit,不牵连任何东西。
席位意见 — (留空,由 skills 席位定稿)
你要做的 — ① 这是受管面(
docs/adr/**,CODEOWNERS 路由),PR 保持 draft,由您手工合并;任何 AI 席位都不会翻 ready、不入合并队列、不挂 auto-merge。② 请特别确认一句话的边界:本 PR 完全没有
碰「这个启动到底该不该被拒绝」这个问题。#17010 是刻意把它留开的,PR #17460 也只做了报告不做拒绝;
本次改动只是把记录里那句已经不成立的事实陈述改成实测事实,那个岔路口仍然完整地摆在您面前,依然是您的。
③ 如果您决定「应该拒绝」,那需要的是一条新的决策(新 ADR 或在旧 ADR 上加修订状态行),不是再改这一句。
Generated by Claude Code