Skip to content

[finding] Form section/field visibleWhen binds no current_user, so position-gated visibility is expressible on pages but not on forms — the asymmetry was undocumented and the docs asserted the opposite (lands in objectui) #11411

Description

@claude

Filed unassigned as a finding — recording only, not graded, no domain:* set.

The code half lands in objectstack-ai/objectui, not here. Filed in this repo because the measuring seat was fenced to content/docs/protocol/objectui/layout-dsl.mdx by its dispatch and routed it to the PM rather than minting a cross-repo card unasked — same disposition as #11258 and #11247. Handing to triage, which owns routing and transfer. The docs half is already corrected on this repo's side by #11034 / PR for claude/issue-11034-visiblewhen-hasrole-example.

Found while implementing #11034 (session session_015ahemw8RcTgqtxrj15PEZx), measured on origin/main at 365e334 with objectui at its current checkout.

The asymmetry

current_user reaches some visibleWhen sites and not others:

Sitecurrent_user bound?Evidence
Page component, app/nav visibleyesapp-shell/src/providers/ExpressionProvider.tsx:59,70 builds { current_user: user, user, ctx: { user }, os: { user }, app, data, features }
Per-option visibleWhen (select options)yescore/src/evaluator/optionRules.ts:103 forwards scope; resolveCascadingOptions(…, predicateScope) in components/src/renderers/form/form.tsx:1428,2129
Form section / fieldvisibleWhennoall three resolveFieldRuleState call sites — components/src/renderers/form/form.tsx:1201, :1237, :1935 — pass undefined for the scope parameter. predicateScope is read at :1276 and used only for resolveCascadingOptions.

evalFieldPredicate (core/src/evaluator/fieldRules.ts:158) binds { record, previous, extra: scope }, so with scope === undefined a form field's predicate sees record and previous and nothing else. Its own fault warning says so: "Values are bound under 'record.' (e.g. record.status)".

Why it is worth recording

  1. It is silent and fail-open in the dangerous direction. A visibleWhen naming an unbound root does not render the element hidden — resolveFieldRuleState passes true as the visibility fallback, so a gate that faults shows the field to everyone. Identical failure shape to layout-dsl.mdx:824 teaches user.hasRole('admin') inside a CEL visibleWhen, but hasRole exists nowhere in spec or core — the example faults and fail-softs to visible #11034, one layer over.
  2. The docs asserted the opposite.content/docs/protocol/objectui/layout-dsl.mdx carried a binding table reading Runtime record forms & pages (*.view.ts, *.page.ts) | record + current_user. An author following it would write 'org_admin' in current_user.positions on a form field and get a field visible to everyone. layout-dsl.mdx:824 teaches user.hasRole('admin') inside a CEL visibleWhen, but hasRole exists nowhere in spec or core — the example faults and fail-softs to visible #11034's PR splits that row.
  3. The platform's own reference already knew.content/docs/references/data/field.mdx:333 (auto-generated from the spec) describes the per-option environment as "the live record plus the host predicate scope, which binds current_userwider than field-level visibleWhen, which has no current_user". So the fact was recorded in one generated page and contradicted in a hand-written one.

The open question triage has to route

Is the missing binding intended or a gap?

⚠️ Note the second option changes shipped behaviour: predicates that today fault-open on form fields would begin evaluating, and any that resolve FALSE would start hiding fields that are currently visible. That is a correctness improvement and a visible change at once, so it wants a deliberate call rather than a drive-by.

Not a duplicate of


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions