Skip to content

Parent-scoped requiredWhen is unenforced server-side — the same gap #4889 closed for readonlyWhen, one slot over #4977

Description

@xuyushun441-sys

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 for readonlyWhen: the server-side strip
now resolves the master-detail header and binds it as parent, so
readonlyWhen: P`parent.status == 'paid'` is enforced where it is documented
to be enforced.

requiredWhen sits 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 requiredWhen branch):

const res = ExpressionEngine.evaluate< boolean >(toExpression(pred), {
  record: merged,
  previous,
  currentUser,
});
if (!res.ok) {
  opts.logger?.warn?.(`requiredWhen for '${name}' failed to evaluate — skipped`);
  // ... rule not applied
}

No parent. So a detail object declaring
requiredWhen: P`parent.status == 'sent'` — "once the header is Sent, every
line 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 field
empty.

Note this is the mirror of #4889's failure mode, not the same one:
readonlyWhen failing open wrote a field that should have been frozen;
requiredWhen failing 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 requiredWhen today, so this is a
latent 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_detail relationship ⇒ that master is parent).
  • 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 predicate
    read parent?" gate; the requiredWhen counterpart is the same helper over a
    different slot.
  • The build-time gate in packages/lint/src/validate-expressions.ts that rejects
    a 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 scope
root ⇒ treat as LOCKED) and recorded it as an ADR-0058 addendum.

The equivalent narrowing for requiredWhen is not symmetric, and that is the
whole question:

  • "Locked" is the conservative answer for readonlyWhen — it preserves stored
    data and costs a caller one rejected field.
  • The conservative answer for requiredWhen is 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:

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 a parent
binding to evaluateValidationRules changes the outcome surface of a different
rule family in the same call (object-level script/cross_field predicates share
that 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.

Activity

  1. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    分诊:转 needs-user-decision,预挂 domain:engine-core。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. claude commented on Aug 6, 2026

    @claude
    Contributor

    裁决(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

  3. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    Contributor

    【裁决落地】维护者 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

  4. self-assigned this
    on Aug 7, 2026
  5. baozhoutao commented on Aug 7, 2026

    @baozhoutao
    Contributor

    认领(执行席 PM,engine-core 车道,第 14 轮)

    按 2026-08-06 维护者裁 A+C 执行。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions