Repository navigation
Parent-scoped requiredWhen is unenforced server-side — the same gap #4889 closed for readonlyWhen, one slot over #4977
Description
Activity
分诊:转
needs-user-decision,预挂domain:engine-core。- 为什么进决策箱而不是队列:缺陷本身具体(
packages/objectql/src/validation/rule-validator.ts的requiredWhen分支无parent绑定,Parent-scopedreadonlyWhenis unenforced server-side — the field lock fails open, so a paid invoice's frozen lines can be rewritten over the API #4889 的管件可直接复用),但修复姿态是 ADR-0058 D5 失败策略在公共 authoring 契约上的拍板——正文已列 A/B/C 三选项并自荐 A+C,且明确写道 "it wants a maintainer's ruling rather than an implementer's preference"。按纪律不代维护者猜。 - 落点无歧义(无论选哪个选项都落 objectql),故预挂 engine-core 域标,裁决后摘
needs-user-decision换pm:queue即可派发。 - 过时前提检查:origin/main 上 rule-validator.ts 自 Parent-scoped
readonlyWhenis unenforced server-side — the field lock fails open, so a paid invoice's frozen lines can be rewritten over the API #4889/PR fix(objectql,lint): 服务端补上 parent 作用域 readonlyWhen 的绑定与执行 (#4889) #4972 后无 requiredWhen 相关改动,前提成立。查重:#4649 的「记录对已声明字段全量」只落在两个接缝上 —— 另外三处求值仍是稀疏绑定 #4953(另外三处稀疏绑定求值)相邻不同题,无重复。
本评论来自分诊座位 Routine(#5474 试点),不构成认领。
Generated by Claude Code
- 为什么进决策箱而不是队列:缺陷本身具体(
裁决(2026-08-06):方案 A+C,并按 ADR-0058 附录追认第四行。
A:服务端给
requiredWhen绑parent(复用 #4889 为 readonlyWhen 建好的resolveMasterDetailParent(s)管件);C:lint 的 parent 作用域校验扩到 requiredWhen(validate-expressions.ts 现刻意只盖 readonlyWhen)。「表达式良构但绑定不到它点名的根 ⇒ fail-closed(拒写)」按 ADR-0058 addendum 既有三行先例(#4649/#4775/#4889)追认为第四行——维护者已授权;requiredWhen 对「谓词本身坏掉」的 fail-soft 保持不动。实现注意(正文警告的爆炸半径):同一求值点的对象级规则自 #4649 起 fail-closed,绑新根必须带 pin 测试防止翻转既有拒写。量级 M。
经办:PM 会话
session_01GcjbQLUQKysMU9uXB34iyv;维护者 2026-08-06 审阅决策简报后授权按建议执行(否决窗口:可评论/重开推翻)。
Generated by Claude Code
【裁决落地】维护者 2026-08-06 批复全舰队决策箱评估报告(批复「同意」),本单裁定:
裁 A + C——服务端绑定 parent 作用域(复用 #4889 为 readonlyWhen 修同型洞时的全部管件),求值语义保持 fail-open;同时上构建期 lint 硬闸(仓内声明必须可静态求值)。B(不可求值 422 拒写,fail-loud)留 ADR-0058 D5 复审场合,不在本单做——header 不可读时拒合法写的副作用需要 ADR 级讨论。
发单人自荐方案获采纳;潜伏缺口(仓内暂无 app 使用),按正常队列节奏派发即可。
流转:摘
needs-user-decision→pm:queue,归 engine-core 车道。评估与落地会话:
session_01N3uGFF8teXbpgtbEJ1aYXu
Generated by Claude Code
认领(执行席 PM,engine-core 车道,第 14 轮)
- 会话:
session_019Q7oc7ASjh8yxyS3Yz78We - 分支:
claude/issue-4977-required-when-parent-scope - 工作树:
../objectstack-issue-4977 - 文件面:objectql 校验层(复用 Parent-scoped
readonlyWhenis unenforced server-side — the field lock fails open, so a paid invoice's frozen lines can be rewritten over the API #4889 为 readonlyWhen 建的 parent 作用域管件)+packages/lint构建期硬闸 + 测试 + changeset;⛔ B 案(422 fail-loud)明确不做(裁决留 ADR-0058 D5 场合)
按 2026-08-06 维护者裁 A+C 执行。
Generated by Claude Code
- 会话:
- added a commit that references this issue
on Aug 8, 2026 - added a commit that references this issue
on Sep 22, 2026 - added a commit that references this issue
on Sep 28, 2026
Found while implementing #4889 (PR #4972). Filed unassigned, deliberately not fixed there — see "Why it was left" below.
Summary
#4889 closed the
parent-scope hole forreadonlyWhen: the server-side stripnow resolves the master-detail header and binds it as
parent, soreadonlyWhen: P`parent.status == 'paid'`is enforced where it is documentedto be enforced.
requiredWhensits on the same field, is evaluated by the same module,and still has the hole.
Evidence
packages/objectql/src/validation/rule-validator.ts→evaluateValidationRules,the field-level conditional block (~L690, the
requiredWhenbranch):No
parent. So a detail object declaringrequiredWhen: P`parent.status == 'sent'`— "once the header is Sent, everyline must carry a description" — evaluates in the inline grid (which binds
parent) and is a no-op on the server: the write is accepted with the fieldempty.
Note this is the mirror of #4889's failure mode, not the same one:
readonlyWhenfailing open wrote a field that should have been frozen;requiredWhenfailing open accepts a record that should have been rejected.Both are
declared ≠ enforced(PD #10), on the same declaration site.No app in this repo authors a parent-scoped
requiredWhentoday, so this is alatent gap rather than a live data defect — the reason it is a normal-priority
issue and not an urgent one.
What #4889 already built that this can reuse
resolveMasterDetailRelation()—packages/objectql/src/master-detail.ts(exactly one
master_detailrelationship ⇒ that master isparent).ObjectQL.resolveMasterDetailParent()/resolveMasterDetailParents()—packages/objectql/src/engine.ts, single-id and batched-bulk header reads,payload-FK-first so a repoint is judged against the master it lands on.
hasParentScopedReadonlyWhenInPayload()— the AST-based "does this predicateread
parent?" gate; therequiredWhencounterpart is the same helper over adifferent slot.
packages/lint/src/validate-expressions.tsthat rejectsa
parent-scoped predicate on an object with no single master.So the plumbing is ~all present. What is NOT decided is the semantics question
below, which is why this was not a "while I'm here" change.
The decision this needs (do not guess it)
requiredWhen's current unevaluable policy is skip (fail-open) —ADR-0058 D5's tier, and #4649 explicitly declined to change it for field
predicates. #4889 narrowed exactly one case for
readonlyWhen(unbound scoperoot ⇒ treat as LOCKED) and recorded it as an ADR-0058 addendum.
The equivalent narrowing for
requiredWhenis not symmetric, and that is thewhole question:
readonlyWhen— it preserves storeddata and costs a caller one rejected field.
requiredWhenis reject the write (422),which is a much louder failure and can block writes on records whose header is
merely unreadable at that moment.
Options:
parent, keep fail-open on unevaluable. Closes the enforcementgap; a header that cannot be read still silently skips the requirement.
Cheapest, and strictly better than today, but leaves a smaller version of the
same hole.
parent, and reject the write when aparent-scopedrequiredWhencannot be evaluated. Fully symmetric with Script validation rules are silently skipped when their predicate fails to evaluate — fail-open is the wrong direction for a validation #4649/hook 的condition求不出值时:全局 fail loud —— 抛错并中断该次操作(方案 B 已拍板;Blocked-by #4770) #4775'sfail-loud direction and with Parent-scoped
readonlyWhenis unenforced server-side — the field lock fails open, so a paid invoice's frozen lines can be rewritten over the API #4889. Costs: an unreadable header 422s anotherwise-legal write.
parentand gate hard at build time only (the lint rule alreadylanded for
readonlyWhen, extended torequiredWhen), keeping runtimefail-open on the assumption the build already rejected the unbindable case.
My reading: A + C is the sound first step (enforcement gap closed, authoring
mistake caught where it is cheap, no new way to 422 a legitimate write), with B
reserved for a deliberate decision by the maintainer alongside the next review of
ADR-0058 D5. But this is a fail-policy question on a public authoring contract,
so it wants a maintainer's ruling rather than an implementer's preference.
Why it was left out of #4972
Scope discipline. #4889's brief was
readonlyWhen, and adding aparentbinding to
evaluateValidationRuleschanges the outcome surface of a differentrule family in the same call (object-level
script/cross_fieldpredicates sharethat evaluation site and have been fail-CLOSED since #4649, so binding a new root
there flips previously-rejected writes to accepted). That is a wider blast radius
than the issue authorized, and it deserves its own change with its own tests.