Skip to content

docs(adr): ADR-0036 服务端 fault 策略收窄,未绑定根一类是 fail-CLOSED (#3888) - #4952

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-3888-adr36-failopen-sentence
Aug 17, 2026
Merged

docs(adr): ADR-0036 服务端 fault 策略收窄,未绑定根一类是 fail-CLOSED (#3888)#4952
yinlianghui merged 1 commit into
mainfrom
claude/issue-3888-adr36-failopen-sentence

Conversation

@yinlianghui

@yinlianghui yinlianghui commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

Fixes #3888

ADR-0036 的 ## Server enforcement (framework) 段把服务端 fault 策略写成无条件成立:

  • A predicate that fails to evaluate is fail-open and logged (a broken rule must never block a legitimate write).

这一句一行覆盖了该段上面两个服务端谓词(requiredWhenreadonlyWhen),而自 objectstack#4889 起它对 readonlyWhen 的一个 fault 类不成立。这是同一个陈旧断言的最后一份未改副本 —— 代码注释那一份(packages/core/src/evaluator/fieldRules.ts 模块头)已由 #3828 / PR #3887 收窄,ADR 是读者问「服务端到底怎么办」时的第一站,留着它等于把刚拆掉的错误心智模型原样保存在更权威的位置。文档改动,无行为、无授权面变化。

前提验证(先行)

卡片正文与晋级评论给的 framework 行号已经漂了两次(正文按 fec784863:599-603/:604/:606-607,晋级评论按 86e6f6c:601 vs :606),按硬规重新定位。以下全部读自 objectstack origin/main @ 23abe2782packages/objectql/src/validation/rule-validator.ts(读法:git -C /home/user/objectstack fetch origin maingit show origin/main:PATH,未碰共享工作树):

事实 重定位后的位置
设计小节「readonlyWhen: the UNBOUND-ROOT case is fail-CLOSED (#4889)」 :99(正文 :99-113)
「Every OTHER readonlyWhen fault … keeps the fail-open policy」 :106-108
isReadonlyWhenLocked :588
未绑定根 → 记日志 treating the field as LOCKED :607-611
同一分支 return true(= LOCKED) :612
其他 fault → failed to evaluate — change allowed through :614
同一分支 return false(= fail-open) :615
stripReadonlyWhenFields 及其 delete :530 / :545
批量孪生 stripReadonlyWhenFieldsMulti 及其 delete :749 / :780
requiredWhen fault(含未绑定根)记日志后 continue —— 仍是 fail-OPEN :1695-1712
「Do NOT copy the fail-CLOSED carve-out」(#4977 裁定) :136

另两条新声称的出处:droppedFieldsreason: 'readonly_when' 见 objectstack packages/objectql/src/engine-dropped-fields-primary-key.test.ts:205(契约来自 objectstack#3794);客户端 readonly fallback 为 false 见本仓 packages/core/src/evaluator/fieldRules.ts:253

premise 成立:那一句仍在,仍是无条件写法,且对 readonlyWhen 的未绑定根一类确实反了方向。

改了什么

  1. 概括那条 bullet 带上例外并指向新增小节 —— 它的职责就是给出正确的一眼概括,所以收窄而非删除。
  2. 新增 ### The one fail-CLOSED fault (objectstack#4889):为什么未绑定根不是「谓词坏了」(是 spec 支持的构造,只是求值点答不上来)、服务端拿它怎么办(判锁死 + 从 payload 删 key + 记 droppedFields)、以及两条非例外:其他 readonlyWhen fault 仍 fail-open(并说明两类 fault 靠引擎错误文本区分,不靠猜),requiredWhen 没有这个 carve-out(#4977 绑了 scope 但刻意保留 fail-open 语义)。
  3. ## Client enforcement (objectui):原文「the same posture as the server」断言的是同一件事,同样收窄,并点明两端在这一格方向相反是刻意的,附上那个静默症状(表单可编辑、保存报成功、值不落库)与该往哪一端排障(服务端 treating the field as LOCKED 警告 + 写响应的 droppedFields,不是客户端谓词)。措辞与 PR docs(core): fieldRules 模块头收窄 readonly fail-open 的「与服务端一致」断言 (#3828) #3887 的 docblock 对齐。

ADR-0057 编号:两层,一层修了一层立案

本仓撞号(已按卡片处理):引用写明是 framework 编号 —— 本仓另有一份无关的 docs/adr/0057-console-ai-chat-one-conversation-docked.md,裸编号会把本仓读者送到错误的文档。

framework 侧 D 锚点不可解(未修,已立案):核验时顺手查了这个引用能不能落地,结论是不能 —— framework 的两份 ADR-0057 里都没有内容对得上的 D10(0057-erp-authorization-core-business-units-and-scope-depth.md:422 的 D10 是 "Setup-nav surfacing follows the capability";0057-system-data-lifecycle-and-retention.md 没有任何 D 编号决策,它用 P0–P4)。规则本身是真的、代码自证,不成立的只是锚点。

所以本 PR 把它写成归属式表述(「the rule the framework cites throughout its rule-validator, its lint diagnostics and its QA runner as ADR-0057 D10」)而不是断言该锚点可解 —— 本卡的全部要点就是不要在权威记录里保存一个查不实的断言,那么在同一次改动里新种一个会很讽刺。framework 侧共 20 个文件 + 一条 adr-anchors/ invariant 骑在这个锚点上,已单独立案 objectstack#9255(观察类,finding,未定级),本 PR 不修。

验证(文档类:正向判据)

  • node scripts/check-control-bytes.mjsOK (scanned 4432 tracked text file(s); skipped 85 binary)
  • 门外自扫 grep -naP + 控制字符类(U+0000–U+0008、U+000B、U+000C、U+000E–U+001F)两个改动文件 → 零命中(本 PR 的散文完全不提及控制字符,仍按规矩自扫)
  • node scripts/check-doc-links.mjsLinks are valid across 13 scan roots.(docs/** 是它的扫描面之一;本 PR 未新增任何链接,ADR-0057 的文件名以反引号给出而非链接)
  • node scripts/check-doc-component-types.mjsEvery documented component type is registered.
  • node scripts/check-changeset-fixed.mjs / check-changeset-no-major.mjs → 均绿
  • 仓根 flock … 'NODE_OPTIONS=--max-old-space-size=4096 pnpm exec turbo run type-check --concurrency=2'81 successful, 81 total(6m11s)。跑在正文定稿前;此后三处编辑全部落在 docs/adr/**.changeset/**,这两处不进任何 tsconfig program(按 --include=tsconfig*.jsondocs/ 前缀零命中),故未复跑。
  • 无新增/更新测试,这是诚实的:改动是一份 markdown ADR,零代码。「新增测试」这一栏对本改动没有可测对象 —— 断言的正确性由上表每条声称配一个 framework file:line 指针来承担,这是文档类改动的正向判据。

改前/改后的逐字对照见 issue 与下方 diff:改前一句 = A predicate that fails to evaluate is **fail-open** and logged (a broken rule must never block a legitimate write).;改后同一 bullet 追加 — **except** a readonlyWhen whose fault is an unbound scope root, which has been fail-CLOSED since objectstack#4889.,细节移入新小节。

分支基于 1b21b1aaa;此后 main 已前移,但 docs/adr/0036-field-conditional-rules.md.changeset/ 在这段区间内无改动(按路径比对为空),无冲突。

草稿 PR,不 undraft。

ADR-0036 的 `## Server enforcement (framework)` 段把服务端 fault 策略写成无条件
成立:「A predicate that fails to evaluate is **fail-open** and logged」。这一句
一行覆盖了两个服务端谓词,而自 objectstack#4889 起它对其中一个 fault 类不成立:
`readonlyWhen` 因为引用了本次写入没有绑定的 scope 根(`parent.status == 'paid'`
而手里没有 master-detail 头)而 fault 时,服务端是 fail-CLOSED ——
`isReadonlyWhenLocked` 记 `… treating the field as LOCKED` 后把字段判为锁死,
`stripReadonlyWhenFields` / `stripReadonlyWhenFieldsMulti` 随即把该 key 从
UPDATE payload 中删除,并以 `reason: 'readonly_when'` 记入写响应的
`droppedFields`。

这是该陈旧断言的最后一份未改副本。代码注释那一份(`packages/core/src/evaluator/
fieldRules.ts` 模块头)已由 #3828 收窄;ADR 是读者问「服务端到底怎么办」时的第
一站,留着它等于把刚拆掉的错误心智模型原样保存在更权威的位置。

概括那条 bullet 现在带上例外并指向新增小节,小节把它讲完:为什么未绑定根不是
「谓词坏了」、服务端拿它怎么办、以及客户端刻意**不**跟随所产生的静默症状(表单
可编辑、保存报成功、值不落库)与该往哪一端排障。`## Client enforcement
(objectui)` 段的「the same posture as the server」原本断言的是同一件事,同样收窄。

两处复核后确认仍准确、未动:`requiredWhen`(objectstack#4977 绑了同一个
`parent` scope 但刻意保留 fail-open 语义,不可求值的 required 在两端都是跳过)与
`visibleWhen`(服务端根本不求值)。二者现在写成显式的**非例外**,而不是由一句过
宽的概括顺带涵盖。

ADR-0057 D10 的引用写明是 **framework** 编号 —— 本仓另有一份无关的
`docs/adr/0057-console-ai-chat-one-conversation-docked.md`,裸编号会把本仓读者送
到错误的文档。同时写成归属式表述(framework 的 rule-validator / lint 诊断 / QA
runner 都这么引)而非断言该 D 锚点可解:核验发现两份 framework ADR-0057 里都没有
内容对得上的 D10,已单独立案 objectstack#9255,本 PR 不修。

Fixes #3888

Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

PM 验收:ACCEPT(#3888,批次 19,PM 会话 session_01GTRjn8xBqp75dk7kFupVRt)

实物核验(已过):2 文件 +100/−2(ADR 本体 + 空 frontmatter changeset);标识 0;releases 0。

实施:无条件「fails to evaluate ⇒ fail-open」句收窄为带 #4889 例外的准确表述 + 新小节完整陈述唯一 fail-CLOSED 故障类,并列两个显式非例外(其它 readonlyWhen 故障保持 fail-open、requiredWhen 无 carve-out 引 #4977)—— 防止读者把例外泛化,写法周全。有据扩界(接受):一节之下的 client parity 从句是同一断言的第二份副本,同笔收窄并新增「两端唯一方向相反处」的排障指引(save SUCCESS 但值不落地的线索指向服务端 droppedFields)—— 留着它就是在卡面目标旁边复刻卡面缺陷。

证据纪律:每个新声称带 framework 行级指针,针对 origin/main 重定位(卡面行号已漂移两轮,如实记录);全部经 git show origin/main: 读取,共享检出零触碰。

ADR-0057 D10 处置(质量点):查证发现该 D-锚点在 framework 两个 0057 文件里都不解析(一个的 D10 是别的决定、另一个用 P 编号)—— 引用改写为归属式(「framework 全线引作 D10 的那条规则」)而非断言式,并立跨仓 finding os#9255(20 文件搭载 + check-adr-anchors 只查号不查字母的门盲区,四轮查重无孪生)。「为消灭不可验证断言而生的卡,不在同笔种新的」—— 正确。

诚实记账:无可测对象故无新测试(如实声明而非制造绿灯);type-check 后三次纯文档措辞编辑未重跑,附「docs 不入任何 tsconfig program」的验证依据 —— 偏离披露合格。

CI(亲读终态):18 项全 completed,16 success + 2 skipped,零失败。

→ undraft + auto-merge (SQUASH)。


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 17, 2026 08:03
@yinlianghui
yinlianghui added this pull request to the merge queue Aug 17, 2026
Merged via the queue into main with commit 4e7570f Aug 17, 2026
19 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-3888-adr36-failopen-sentence branch August 17, 2026 08:03
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

Projects

None yet

2 participants