Skip to content

FormField/FormSection visibleWhen describe 宣称 runtime 表单绑定 current_user,但字段级条件规则的求值面从未绑过它——按文档写的谓词必然 fault 并 fail-open #6146

Description

@hotlong

现象

packages/spec/src/ui/view.zod.ts 的字段级与 section 级 visibleWhen 文档(origin/main):

  • :1410-1412(JSDoc)与 :1414(describe):「Root: record+current_user (runtime forms) or data (metadata forms)」
  • :1505-1508:FormSection 级同样宣称

但 字段级条件规则的实际求值面没有任何一处绑定 current_user:

  • objectui 侧:resolveFieldRuleState 的三个生产调用点(packages/components/src/renderers/form/form.tsx 渲染门与 error-clearing effect、packages/plugin-form/src/WizardForm.tsx 必填门)都不传 scope,evalFieldPredicate 只绑 record / previous /(经 extra 的)parent。objectui 的授权 autocomplete 也是这么钉的:packages/app-shell/src/views/metadata-admin/celAuthoring.ts 的 FIELD_RULE_ROOTS = ['record', 'previous', 'parent'],注释明说 "nothing else (no current_user)"(objectui#1582 的决定);
  • 服务端:packages/objectql/src/validation/rule-validator.ts 的 EvaluateRulesOptions.currentUser 按其 JSDoc 只供 per-option visibleWhen(objectui#2284,ADR-0068);readonlyWhen 求值(isReadonlyWhenLocked)绑 record/merged + parent,不含 current_user。

per-option visibleWhen(选项级)确实绑 current_user——文档对「选项级」为真,对「字段级/section 级」为假。

后果

照 describe 写 visibleWhen: "current_user.positions.exists(p, p == 'admin')" 的字段级谓词,引用未绑定根 → fault → fail-open(字段恒可见)。这正是 #5149 的「文档诱导写出永不生效谓词」类别(Repro 1 是裸标识符,这里是文档亲自点名的根)。objectui PR objectstack-ai/objectui#3541(#5149 诉求 2)落地后该 fault 会以每谓词一条 console.warn 显形(undeclared reference to current_user 一类),不再静默,但谓词本身依旧不工作。

两个修法方向(留给分诊)

  1. 文档收窄:describe 与 JSDoc 把字段级/section 级的 runtime 根改为 record(+previous/parent),current_user 只保留在 per-option visibleWhen 的文档上——与 objectui#1582 钉的授权面一致,改动最小;
  2. 实现补齐:让字段级规则求值面(客户端 resolveFieldRuleState 调用点传 host predicate scope;服务端 rule-validator 同步)真的绑 current_user——有真实业务拉力(按角色显隐字段)时才值得,且需两端同时落,否则客户端/服务端 verdict 漂移。

按仓规不自选方向,留 PM 分诊。发现于 #5149 诉求 4 验收(该验收本身成立:record./data. 绑定说明与示例齐全,本单只针对 current_user 这一半句的过度宣称)。

Refs: #5149、objectstack-ai/objectui#3541、objectui#1582、objectui#2284、ADR-0068

Activity

  1. claude commented on Aug 7, 2026

    @claude
    Contributor

    Triage: pm:queue + domain:spec + target:v17.

    Landing site (read from code, not from the title). The over-claim is four strings in packages/spec/src/ui/view.zod.ts — verified verbatim on origin/main just now: :1411 (field JSDoc), :1414 (field describe), :1505 (section JSDoc), :1508 (section describe), each carrying Root: `record`+`current_user` (runtime forms). That is packages/spec ⇒ domain:spec. The evaluation surfaces the body cites (objectui resolveFieldRuleState call sites, rule-validator.ts) are evidence that the claim is false, not the fix site — under direction 1 nothing outside packages/spec changes.

    Why pm:queue and not the decision box. The contract question this looks like it raises is already answered, twice, and both answers point the same way: objectui#1582 pinned FIELD_RULE_ROOTS = ['record', 'previous', 'parent'] with an explicit "nothing else (no current_user)", and ADR-0068 / objectui#2284 scope EvaluateRulesOptions.currentUser to per-option visibleWhen. So direction 1 (narrow the describe/JSDoc to record + previous/parent, leave current_user documented only on per-option visibleWhen) sits inside rulings that already exist — it is a concrete defect with a named location, not an appetite question.

    Direction 2 (actually bind current_user on the field/section evaluation surface) is the half that would need a maintainer call, and it is deliberately not queued here: it is cross-repo, needs client and server to land together or verdicts drift, and the issue itself conditions it on "有真实业务拉力". If that pull appears, it is a fresh card, not a re-scope of this one.

    target:v17. Criterion ② — a public contract surface whose declaration is a lie. It is also a member of the family #5149 already carries target:v17 for: a predicate written from this documentation faults and fails open, so the field stays visible instead of hiding. The doc does not merely omit the root, it names it as an example.

    Stale-premise check. git log origin/main -- packages/spec/src/ui/view.zod.ts — most recent touch is 820ea5d (a CI change, unrelated); all four line anchors still read as filed.

    Dedup. Full paginated enumeration of open issues and PRs in all three repos (the GitHub search API is not reachable from this seat — repository-scoped sessions get This GitHub API path is not available). Nearest neighbours are #5149 (parent fail-open family, pm:queue + repo:objectui + target:v17) and #6128 (the build-time bare-identifier lint split out of #5149's ruling, domain:devx). Neither overlaps: #5149/#6128 make a broken predicate visible, this one stops the schema from teaching readers to write one. No duplicate; cross-linked only.

    Serialization note for whoever claims it. #6128 lands in packages/lint, not in view.zod.ts — no file collision. But a describe edit in packages/spec regenerates docs/baselines; check the merge=os-regen paths before batching this alongside another spec-lane item.

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


    Generated by Claude Code

  2. hotlong commented on Aug 7, 2026

    @hotlong
    ContributorAuthor

    补一份构建期实测,给本单的裁决多一个面(不构成认领)。来自 #6128 / PR #6248 —— 那里为了给 view/page 谓词的裸标识符闸门定合法根集合,把 current_user 逐层测了一遍。

    本单记的是运行时从未绑过 current_user。构建期这一侧方向相反、同样是坏的:lint 会因为作者按文档写了 current_user 而直接拒收构建,并给出一条错误的修法。

    const { validateStackExpressions } = require('@objectstack/lint');
    validateStackExpressions({ objects: [{ name: 'acct',
      fields: { tier: { type: 'select', visibleWhen: "'admin' in current_user.positions" } } }] });

    实测输出:

    error | object 'acct' · field 'tier' visibleWhen
      bare reference `current_user` — a formula/validation expression binds the record as the
      `record` namespace, not at top level, so `current_user` resolves to nothing and the
      expression silently evaluates to null. Write `record.current_user`.
    

    三点值得记进本单:

    1. 被拒的写法正是 spec 自己给的示例。 packages/spec/src/data/field.zod.ts:136 的 describe 原文就把 P`'admin' in current_user.positions` 列为 option visibleWhen 的例子;字段级 visibleWhen 的绑定环境按 :125 与 view.zod.ts:1416 也写着 record + current_user。
    2. 给出的修法是错的。 record.current_user 在任何一层都不是绑定 —— 这条消息会把作者推到一个更坏的写法上。
    3. 根因在一个包内部自相矛盾。 packages/formula/src/cel-engine.ts 的 SCOPE_ROOTS 列了 user 但没有 current_user;而同包 validate.ts 的 introspectScope 对外宣告的 roots 包含 current_user,checkRoleCatalog 的四条正则也全是按 current_user 写的。同一个包,一边宣告它是合法根,一边把它判成裸引用。

    之所以至今没人撞上:仓内示例只在每个选项的 visibleWhen 上用过这个写法(examples/app-showcase/src/data/objects/cascading-select.object.ts:85),而 validate-expressions.ts 只遍历字段级 visibleWhen,不遍历 option 级 —— 差一层就绕过了。

    所以本单裁 current_user 时,建议把三件事一起裁,否则会各修一半:运行时绑不绑、spec 文档怎么写、以及 SCOPE_ROOTS 要不要收下它。若裁「保留 current_user 并在运行时绑上」,SCOPE_ROOTS 加一个词即可;若裁「退役这个根」,则 field.zod.ts:136 的示例、view.zod.ts 两处 describe 和 introspectScope 的宣告都要一起改。

    PR #6248 的新闸门在这期间把 current_user 声明为合法根、不报它 —— 那个根到底解不解析是本单的裁决,新规则不替它先斩。


    Generated by Claude Code

  3. hotlong commented on Aug 7, 2026

    @hotlong
    ContributorAuthor

    Triage: domain:spec → domain:spec-surface (seat #6298, split approved 2026-08-07; existing-stock migration executed by this pass on the maintainer's instruction). Admission test met — the describe teaches a binding that was never bound, so correcting it leaves every metadata document byte-identically as legal or illegal as before; changeset patch, zero migration. This card is named in the seat post itself as the surface half of the #6146 / #6290 pair (#6290 stays domain:spec-tooling: the lint judging wrongly and prescribing the wrong fix).


    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

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions