Skip to content

Form section/field visibleWhen binds no current_user — position-gated visibility works on pages and per-option rules, but silently fail-opens on form fields #6010

Description

@os-zhuang

Rebuilt from objectstack-ai/objectstack#11411 by the triage seat (file-at-destination — the code half lands in this repo; the transfer API is unavailable to agent seats, so this is the documented rebuild channel; the source card is closed as moved). Originally measured by session session_015ahemw8RcTgqtxrj15PEZx while implementing objectstack-ai/objectstack#11034, on objectstack origin/main @ 365e334 with objectui at its then-current checkout. Re-verify every line reference before editing.

The asymmetry

current_user reaches some visibleWhen sites and not others:

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

evalFieldPredicate (packages/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.

Why it matters

  1. Silent and fail-open in the dangerous direction. A visibleWhen naming an unbound root does not hide the element — resolveFieldRuleState passes true as the visibility fallback, so a gate that faults shows the field to everyone. Same failure shape as objectstack#11034, one layer over.
  2. The docs taught the broken pattern until objectstack#11034's PR split the binding-table row; the generated reference (field.mdx:333) already recorded the asymmetry.

Triage direction anchor (graded, not ruled here)

ADR-0068 D1's stated goal — "a predicate authored against any one form evaluates identically" — is the declared contract this asymmetry violates, and the fix is small and local: pass predicateScope (already in hand at form.tsx:1276) as the scope argument to the three resolveFieldRuleState calls, exactly as resolveCascadingOptions already receives it. Graded Bug on that basis: declared≠enforced restoration, not new surface.

Constraints for the implementing dev:

  • Premise-first stop: if you find a recorded ruling that form-field predicates deliberately exclude current_user (the record-purity/per-keystroke-cost design story the source card sketches), stop and report a fork — ⛔ do not pick a side silently.
  • Behaviour-change honesty: predicates that today fault-open on form fields will begin evaluating, and any resolving FALSE will start hiding fields that are currently visible. That is the correctness restoration, but it must be loud: changeset names the change; add a pin per binding surface so the three-site parity cannot silently regress.
  • Cross-repo docs follow-up (联动杂事): objectstack's content/docs/protocol/objectui/layout-dsl.mdx binding table was just split (#11034) to document today's asymmetry. When this lands, that row is wrong again in the other direction — the accepting seat files the follow-up docs card in objectstack at landing time (with Blocked-by: this card), ⛔ not as a rider on this PR.

Not a duplicate of

Metadata

Metadata

Assignees

Labels

domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpm:dispatched

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions