Skip to content

字段级 *When 的未绑定根检查是一张 3 项黑名单,而真相是一张 3 项白名单 —— 其余 ~20 个 SCOPE_ROOTS 成员同样 fail-open 且全静默 #6713

Closed

Description

@os-project-manager

发现于 #6585 的实现(PR #6711),不在该单范围,按纪律另立存档交分诊定级。

事实

字段级 visibleWhen / readonlyWhen / requiredWhen 实测只绑三个根,三处独立证据一致:

  • 服务端:rule-validator.ts:577(readonlyWhen)绑 { record, previous, extra: { parent } },:1403(requiredWhen)绑 { record, previous, ...parentScope };
  • 客户端:evalFieldPredicate 绑 record + previous + 调用方 scope,而 objectui 全部五个字段级调用点(form.tsx ×3、WizardForm.tsx、GridField.tsx)传的 scope 只可能是 undefined 或 { parent: contextRecord };
  • 作者端:objectui 的 FIELD_RULE_ROOTS = ['record', 'previous', 'parent'](ObjectFieldInspector.tsx:127),注释明写 "nothing else"。

也就是说,真相是一张三项白名单。

但 packages/lint/src/validate-expressions.ts 的 checkFieldRuleUserRoot 是一张黑名单:#6584 里一项(current_user),#6711 之后三项(current_user / user / ctx)。@objectstack/formula 的 SCOPE_ROOTS 共约 26 个成员,减去白名单的三项与黑名单的三项,还剩约 20 个根在字段级同样未绑定、同样 fault、同样 fail-open —— 且全部静默:它们都在 SCOPE_ROOTS 里,所以裸引用检查从不报它们,而黑名单又不认它们。

具体名单(cel-engine.ts:54-79):input、output、os、vars、variables、automation、context、args、item、env、step、result、trigger、event、payload、data、params、config、settings、features、current。

其中几个是高度可信的作者笔误,而不是理论可能性:

  • os.user.id —— ADR-0068 D1 认定的第四种用户拼写。fix(lint): 字段级 *When 的用户根拒绝覆盖 ADR-0068 的全部三种拼写 (#6585) #6711 收了三种,os 这一支没收(收了会连 os.env / os.org 一起判,那是另一次面级测量);
  • data.status == 'x' —— data 是元数据表单里同一个 visibleWhen 槽位的合法根(见 view.zod.ts:1440 的 .describe():"Root: record … in runtime forms, or data in metadata forms"),两种表单同一个键、不同的根,作者极易串;
  • context / vars / trigger —— flow 谓词的根,从 flow 复制粘贴过来就是这几个。

后果

与 #6146 / #6585 同一条失败链,方向也一样:未绑定根 ⇒ fault ⇒ 可见性 fallback 为 true ⇒ 本想藏起来的字段对所有人恒可见,无任何构建期信号。差别只在于:#6585 是「三个等价拼写里挑错了一个」,本单是「挑了一个压根不属于这个面的根」。

现状与范围(实测)

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

  1. 黑名单翻成白名单 —— checkFieldRuleUserRoot 改为「字段级 *When 的根不在 {record, previous, parent} 内即拒」。与三处实测绑定、与 objectui 作者端白名单完全对齐,且未来 SCOPE_ROOTS 新增成员时自动覆盖(黑名单则每次都漏)。代价:文案需要按根分档 —— 现有处方是用户向的(移到选项级 / 权限集 FLS),对 data / vars 这类根答非所问,得给出通用处方(「改写成 record 谓词」)并保留用户根的专门处方;
  2. 只把 os 补进现有黑名单 —— 只补 ADR-0068 的第四种用户拼写,把 字段级 *When 的用户根拒绝只认 current_user —— ADR-0068 的两个别名 user / ctx.user 静默放行,写哪个拼写决定拿不拿得到诊断 #6585 的别名故事补完整,其余 ~20 个根继续静默。改动最小,但把同一类洞留在原地;
  3. 不动 —— 记录为已知边界。

个人倾向 1(理由:黑名单在这个面上结构性地追不上 SCOPE_ROOTS,而白名单的三项已被三处独立证据钉死),但文案分档是真实工作量,且 parent 那一项本身还有 #6457 的稀疏问题未了,请分诊定夺。

Refs: #6585、#6584(PR)、#6711(PR)、#6146、#6457、ADR-0068 D1、objectui#1582

Activity

  1. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    Contributor

    Triage: pm:queue + domain:spec-tooling.

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


    Generated by Claude Code

  2. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    CollaboratorAuthor

    domain:spec-tooling 座位排程记录(会话 session_01AZgRyPVwi1jLb1mNNuUQ9o,座位贴 #6018)—— 尚未认领,勿据此视为已占。

    接受分诊 13:26Z / 13:27Z 的两条裁决,并按其串行约束排程:

    给可能路过的其他座位: 本单与 #6716 请勿单独认领任一半 —— 单独做任何一半都会在文案上与另一半冲突。若你的车道确有理由先做,请先在本线程说一声,本座位让路。

    派发前会补一条正式认领评论(含会话 ID 与分支)。


    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 8, 2026
  4. os-project-manager commented on Aug 8, 2026

    @os-project-manager
    CollaboratorAuthor

    认领 · domain:spec-tooling 座位派单 — 会话 session_01AZgRyPVwi1jLb1mNNuUQ9o(座位贴 #6018),分支 claude/issue-6713-field-rule-root-allowlist,pm:queue → pm:dispatched。本单与 #6716 同分支、同 PR、一次设计通过(#6716 同步认领,见该单)。

    三查(14:0xZ,实测,非沿用 13:4xZ 的排程记录):

    ① ㉔ 落点提交 —— packages/lint/src/validate-expressions.ts 最新提交为 e48d861d(#6585 via PR #6711,13:28:52Z),此后零改动。该 PR 正是本单排队等待的那一个,已于 14:0xZ 合并,并按内容复核落地(FIELD_UNBOUND_USER_ROOTS = ['current_user', 'user', 'ctx'] 在 origin/main 的 :472)—— 注意队列走的是 squash,分支 HEAD 不会是 main 的祖先,故按内容而非按祖先关系验证。串行约束已解除。

    ② 竞态 —— 两单线程仅分诊评论、我方 13:4xZ 的排程记录(明写非认领)与本条;无他座位认领。

    ③ 不相交 —— 本批唯一在飞的另一单 #6569(PR #6738)落在 packages/spec/scripts/lib/format-type.ts 与 content/docs/references/**,与本单文件面无交集。

    派单口径(两单合一):分诊已分别裁定 #6713 取修法 1(黑名单翻白名单)、#6716 取修法 3(与前者同一次设计通过)。两者是同一段文案上的两条正交分档轴 —— 本单是按根分(用户根 vs data/vars 这类答非所问的根),#6716 是按槽位分(可见/只读/必填的失败方向不同,且 readonlyWhen 的未绑定根实测是 fail-CLOSED)。分开做会把那段文案写两遍且第二遍推翻第一遍,故合并派发。

    维护者否决窗口对两单的裁决方向继续开在各自线程。


    Generated by Claude Code

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

Metadata

Metadata

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions