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:
| Site | current_user bound? | Evidence |
|---|
Page component, app/nav visible | yes | packages/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 | packages/core/src/evaluator/optionRules.ts:103 forwards scope; resolveCascadingOptions(…, predicateScope) in packages/components/src/renderers/form/form.tsx:1428,2129 |
Form section / fieldvisibleWhen | no | all 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
- 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. - 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
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_015ahemw8RcTgqtxrj15PEZxwhile implementing objectstack-ai/objectstack#11034, on objectstackorigin/main@ 365e334 with objectui at its then-current checkout. Re-verify every line reference before editing.The asymmetry
current_userreaches somevisibleWhensites and not others:current_userbound?visiblepackages/app-shell/src/providers/ExpressionProvider.tsx:59,70builds{ current_user: user, user, ctx: { user }, os: { user }, app, data, features }visibleWhen(select options)packages/core/src/evaluator/optionRules.ts:103forwardsscope;resolveCascadingOptions(…, predicateScope)inpackages/components/src/renderers/form/form.tsx:1428,2129visibleWhenresolveFieldRuleStatecall sites —form.tsx:1201,:1237,:1935— passundefinedfor thescopeparameter;predicateScopeis read at:1276and used only forresolveCascadingOptionsevalFieldPredicate(packages/core/src/evaluator/fieldRules.ts:158) binds{ record, previous, extra: scope }, so withscope === undefineda form field's predicate seesrecordandpreviousand nothing else.Why it matters
visibleWhennaming an unbound root does not hide the element —resolveFieldRuleStatepassestrueas the visibility fallback, so a gate that faults shows the field to everyone. Same failure shape as objectstack#11034, one layer over.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 atform.tsx:1276) as thescopeargument to the threeresolveFieldRuleStatecalls, exactly asresolveCascadingOptionsalready receives it. Graded Bug on that basis: declared≠enforced restoration, not new surface.Constraints for the implementing dev:
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.content/docs/protocol/objectui/layout-dsl.mdxbinding 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 (withBlocked-by:this card), ⛔ not as a rider on this PR.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 #5926 (three othervisibleWhengaps:emptyActionbypass, second evaluator, silence on fault) — different sites; none is the form-field scope argument.describeunder-names page-layer roots — the layer where the roots are bound).