You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
[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
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:
Site
current_user bound?
Evidence
Page component, app/nav visible
yes
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)
yes
core/src/evaluator/optionRules.ts:103 forwards scope; resolveCascadingOptions(…, predicateScope) in components/src/renderers/form/form.tsx:1428,2129
Form section / fieldvisibleWhen
no
all 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)".
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_user — wider 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.
If a gap — the fix in objectui 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. That would make one predicate text mean the same thing on every visibleWhen surface, which is the stated ADR-0068 D1 goal — "a predicate authored against any one form evaluates identically".
⚠️ 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.
Filed unassigned as a
finding— recording only, not graded, nodomain:*set.The code half lands in
objectstack-ai/objectui, not here. Filed in this repo because the measuring seat was fenced tocontent/docs/protocol/objectui/layout-dsl.mdxby 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 forclaude/issue-11034-visiblewhen-hasrole-example.Found while implementing #11034 (session
session_015ahemw8RcTgqtxrj15PEZx), measured onorigin/mainat 365e334 with objectui at its current checkout.The asymmetry
current_userreaches somevisibleWhensites and not others:current_userbound?visibleapp-shell/src/providers/ExpressionProvider.tsx:59,70builds{ current_user: user, user, ctx: { user }, os: { user }, app, data, features }visibleWhen(select options)core/src/evaluator/optionRules.ts:103forwardsscope;resolveCascadingOptions(…, predicateScope)incomponents/src/renderers/form/form.tsx:1428,2129visibleWhenresolveFieldRuleStatecall sites —components/src/renderers/form/form.tsx:1201,:1237,:1935— passundefinedfor thescopeparameter.predicateScopeis read at:1276and used only forresolveCascadingOptions.evalFieldPredicate(core/src/evaluator/fieldRules.ts:158) binds{ record, previous, extra: scope }, so withscope === undefineda form field's predicate seesrecordandpreviousand nothing else. Its own fault warning says so:"Values are bound under 'record.' (e.g. record.status)".Why it is worth recording
visibleWhennaming an unbound root does not render the element hidden —resolveFieldRuleStatepassestrueas the visibility fallback, so a gate that faults shows the field to everyone. Identical failure shape tolayout-dsl.mdx:824teachesuser.hasRole('admin')inside a CELvisibleWhen, buthasRoleexists nowhere in spec or core — the example faults and fail-softs to visible #11034, one layer over.content/docs/protocol/objectui/layout-dsl.mdxcarried a binding table readingRuntime record forms & pages (*.view.ts, *.page.ts) | record + current_user. An author following it would write'org_admin' in current_user.positionson a form field and get a field visible to everyone.layout-dsl.mdx:824teachesuser.hasRole('admin')inside a CELvisibleWhen, buthasRoleexists nowhere in spec or core — the example faults and fail-softs to visible #11034's PR splits that row.content/docs/references/data/field.mdx:333(auto-generated from the spec) describes the per-option environment as "the liverecordplus the host predicate scope, which bindscurrent_user— wider than field-level visibleWhen, which has nocurrent_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?
layout-dsl.mdx:824teachesuser.hasRole('admin')inside a CELvisibleWhen, buthasRoleexists nowhere in spec or core — the example faults and fail-softs to visible #11034 is the whole answer, and this card closes as documented behaviour. There is a defensible design story: a form field predicate is evaluated per keystroke against live form values, and keeping its scope minimal keeps it cheap and record-pure.predicateScope(already in hand atform.tsx:1276) as thescopeargument to the threeresolveFieldRuleStatecalls, exactly asresolveCascadingOptionsalready receives it. That would make one predicate text mean the same thing on everyvisibleWhensurface, which is the stated ADR-0068 D1 goal — "a predicate authored against any one form evaluates identically".Not a duplicate of
visibleWhengate found by census: one authored-node bypass, a second evaluator with an oppositedatabinding, and total silence on fault in production (all land in objectui) #11258 — three othervisibleWhengaps in objectui (emptyActionbypass, a second evaluator with an oppositedatabinding, silence on fault). Different sites; none is the form-field scope argument.PageComponentSchema.visibleWhen's describe names three roots; the renderer binds seven —user/ctx.user/os.user/app/featuresare undocumented on that surface #11256 — the specdescribeforPageComponentSchema.visibleWhenunder-names the roots the renderer binds. That is the page layer, where the roots are bound; this is the form layer, where they are not.Generated by Claude Code