Skip to content

字段级未绑定根的诊断对三个槽位共用一句「falls back to VISIBLE」——而 readonlyWhen 的未绑定根实测是 fail-CLOSED(#4889),方向刚好相反 #6716

Description

@os-project-manager

PM 座位在验收 PR #6711(#6585)时的越界发现,不由该 PR 引入,按 PD #10 立案,未认领、未定级。与 #6713 同现场但不同条(见下「与 #6713 的关系」)。

事实(实测,origin/main)

packages/lint/src/validate-expressions.tscheckFieldRuleUserRootvisibleWhen / readonlyWhen / requiredWhen 三个槽位共用一条文案,其中的因果句是:

${root} is unbound here, so the predicate faults and falls back to VISIBLE, leaving the field the test was meant to hide showing for everyone (#6146).

readonlyWhen 的未绑定根在服务端不是 fail-open。packages/objectql/src/validation/rule-validator.ts 的模块头自陈(:99-110):

readonlyWhen: the UNBOUND-ROOT case is fail-CLOSED (#4889)

… A readonlyWhen predicate that faults because it names a scope ROOT this operation did not bind … That single case now resolves to LOCKED. Every OTHER readonlyWhen fault (undeclared key, null overload, parse error) keeps the fail-open policy … and requiredWhen / option visibleWhen are untouched.

实现一致 —— isReadonlyWhenLocked(:399-428)在 unknownVariableOf(res.error) 命中时:

logger?.warn?.(
  `readonlyWhen for '${name}' reads '${unbound}', which is not bound for this operation — ` +
    `treating the field as LOCKED (the declared lock is not waived because it could not be evaluated). ` + 
);
return true;

未绑定根正是这条 carve-out 命中的那一类(而不是「undeclared key / parse error」那一类),所以一个写了 readonlyWhen: "'admin' in user.positions" 的字段,服务端的后果是被锁死,不是「对所有人可见」。诊断把失败方向讲反了。

边界:哪些已测、哪些没测

诚实分档,不把未测的当已测:

所以准确的说法是:这句因果只对 visibleWhen 精确,对另外两个槽位至少不精确、对 readonlyWhen 服务端是反的。

为什么记下来(而不是当文案小事)

处方是对的(移到选项级 visibleWhen / 用权限集 FLS),作者照着做仍能修好。问题在理由那半:本仓反复把「诊断说真话」当契约面对待(ADR-0078 的读者面、validate-security-posture.ts:52-55 那段「inert branch 读起来像一个在看着的门」)。一条把失败方向讲反的诊断,会让作者对「不修会怎样」形成相反的心智模型 —— 对 readonlyWhen 尤其要命:他以为「不修 = 字段泄漏」,实际是「不修 = 字段被锁,写不进去」,两者的紧迫性与排障方向完全不同。

不是 PR #6711 引入的

明确记录,免得被当成回归:#6711 的 diff 只把 current_user 换成 ${root} 插值。这句因果与「三个槽位共用一条文案」都来自 #6584 / #6290,早于本次改动。#6711 只是让它更容易被命中(拼写从一种变三种),并顺带给它加了钉子(新测试 toMatch(/\\w+` reads `user`/)` 断言的是根名,不碰这句因果)。⛔ 不构成阻塞 #6711 的理由,该 PR 是严格的改进。

#6713 的关系(相邻,不重复)

#6713 讲的是根集合的形状 —— 黑名单 3 项 vs 实测白名单 3 项,其余 ~20 个 SCOPE_ROOTS 成员同样静默。本条讲的是已命中之后那句话本身讲错了方向。两者可以各自独立成立:把黑名单翻成白名单(#6713 修法 1)不会自动修好这句因果,反过来改文案也不会多认出一个根。

两者在文案分档上会合流:#6713 修法 1 已经指出「现有处方是用户向的,对 data / vars 这类根答非所问,得按根分档」。本条追加一条正交的分档轴 —— 按槽位分(可见性 / 只读 / 必填的失败方向各不相同)。若分诊决定做 #6713 修法 1,建议把这两轴一起设计,否则会改两遍同一段文案。

可能的修法(留给分诊,不自选)

  1. 按槽位分档因果句:visibleWhen 保留现文案;readonlyWhen 改为「faults ⇒ 该字段被视为 LOCKED(Parent-scoped readonlyWhen is unenforced server-side — the field lock fails open, so a paid invoice's frozen lines can be rewritten over the API #4889),声明的锁不会因为算不出来而被放行」;requiredWhen 按实测结果补。需要先把上面「未测」的两格量出来。
  2. 把因果句降格为槽位无关的表述(例如「谓词 fault,该规则的结果由各槽位的 fault 策略决定,均非你声明的那个」),牺牲具体性换取正确性。成本最低,但也最不像本仓的文案风格 —— 本仓的诊断一贯给具体后果。
  3. 字段级 *When 的未绑定根检查是一张 3 项黑名单,而真相是一张 3 项白名单 —— 其余 ~20 个 SCOPE_ROOTS 成员同样 fail-open 且全静默 #6713 修法 1 合并设计(推荐先决:若 字段级 *When 的未绑定根检查是一张 3 项黑名单,而真相是一张 3 项白名单 —— 其余 ~20 个 SCOPE_ROOTS 成员同样 fail-open 且全静默 #6713 走白名单,文案必然要重写,此时两轴一起做)。

关联

#6585#6584(PR)、#6711(PR,发现现场)、#6713(同现场姊妹条)、#6146#4889(fail-closed carve-out)、#6457(parent 稀疏)、ADR-0057 D10、ADR-0058 D5(被 #4889 收窄的那条)、ADR-0078。

查重:readonlyWhen fail-closed 文案falls back to VISIBLE#4889 diagnostic direction 三次检索,无同题单;#6713 如上分析为相邻非重复。

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions