docs(adr): ADR-0036 服务端 fault 策略收窄,未绑定根一类是 fail-CLOSED (#3888) - #4952
Conversation
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>
|
PM 验收:ACCEPT(#3888,批次 19,PM 会话 实物核验(已过):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 重定位(卡面行号已漂移两轮,如实记录);全部经 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 |
Fixes #3888
ADR-0036 的
## Server enforcement (framework)段把服务端 fault 策略写成无条件成立:这一句一行覆盖了该段上面两个服务端谓词(
requiredWhen与readonlyWhen),而自 objectstack#4889 起它对readonlyWhen的一个 fault 类不成立。这是同一个陈旧断言的最后一份未改副本 —— 代码注释那一份(packages/core/src/evaluator/fieldRules.ts模块头)已由 #3828 / PR #3887 收窄,ADR 是读者问「服务端到底怎么办」时的第一站,留着它等于把刚拆掉的错误心智模型原样保存在更权威的位置。文档改动,无行为、无授权面变化。前提验证(先行)
卡片正文与晋级评论给的 framework 行号已经漂了两次(正文按
fec784863写:599-603/:604/:606-607,晋级评论按86e6f6c写:601vs:606),按硬规重新定位。以下全部读自 objectstackorigin/main@23abe2782的packages/objectql/src/validation/rule-validator.ts(读法:git -C /home/user/objectstack fetch origin main后git show origin/main:PATH,未碰共享工作树):readonlyWhen: the UNBOUND-ROOT case is fail-CLOSED (#4889)」:99(正文:99-113)readonlyWhenfault … keeps the fail-open policy」:106-108isReadonlyWhenLocked:588treating the field as LOCKED:607-611return true(= LOCKED):612failed to evaluate — change allowed through:614return false(= fail-open):615stripReadonlyWhenFields及其delete:530/:545stripReadonlyWhenFieldsMulti及其delete:749/:780requiredWhenfault(含未绑定根)记日志后continue—— 仍是 fail-OPEN:1695-1712:136另两条新声称的出处:
droppedFields的reason: 'readonly_when'见 objectstackpackages/objectql/src/engine-dropped-fields-primary-key.test.ts:205(契约来自 objectstack#3794);客户端 readonly fallback 为false见本仓packages/core/src/evaluator/fieldRules.ts:253。premise 成立:那一句仍在,仍是无条件写法,且对
readonlyWhen的未绑定根一类确实反了方向。改了什么
### The one fail-CLOSED fault (objectstack#4889):为什么未绑定根不是「谓词坏了」(是 spec 支持的构造,只是求值点答不上来)、服务端拿它怎么办(判锁死 + 从 payload 删 key + 记droppedFields)、以及两条非例外:其他readonlyWhenfault 仍 fail-open(并说明两类 fault 靠引擎错误文本区分,不靠猜),requiredWhen没有这个 carve-out(#4977 绑了 scope 但刻意保留 fail-open 语义)。## 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.mjs→OK (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.mjs→Links are valid across 13 scan roots.(docs/**是它的扫描面之一;本 PR 未新增任何链接,ADR-0057 的文件名以反引号给出而非链接)node scripts/check-doc-component-types.mjs→Every 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*.json搜docs/前缀零命中),故未复跑。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。