Uh oh!
There was an error while loading. Please reload this page.
fix(components): bind current_user on form section/field visibleWhen predicates - #6037
Merged
Merged
Conversation
Form section and field `visibleWhen` predicates now evaluate against the host
shell's predicate scope, the same bag per-option `visibleWhen` and the
page/app-nav node gate already receive.
All five `resolveFieldRuleState` / `evalFieldPredicate` call sites for AUTHORED
predicates in the form renderer passed `undefined` for `scope`, so a form-field
gate naming `current_user` named an unbound root — and the visibility fallback
is fail-open, so the gate showed the field to everyone instead of hiding it.
`usePredicateScope()` moves above `readonlyFieldNames`: that memo factory runs
synchronously during render, so the hook's historical position ~75 lines below
put it in the temporal dead zone at that call site. The hook is still called
unconditionally, once per render.
The synthesised legacy `condition: { field, equals }` predicate keeps
`undefined` — it can only ever name `record.<field>`.
Adds a parity pin running one authored predicate text through all five binding
surfaces in three modes: denied (hides), allowed (still renders), faulted
(still fails open).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7LContributor
✅ Console Performance Budget
The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it. 📦 Bundle Size Report
Size Limits
|
This was referenced Aug 24, 2026
yinlianghui
marked this pull request as ready for review
August 24, 2026 14:23
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#6010
What was wrong
current_userreached two of the threevisibleWhenbinding surfaces and not the third.A page component or app/nav node got it (
ExpressionProvider→SchemaRenderer), aper-option rule got it (
resolveCascadingOptions(…, predicateScope)), and everyresolveFieldRuleStatecall in the form renderer passedundefinedforscope— so aform section or field predicate saw
recordandpreviousand nothing else.resolveFieldRuleStatepassestrueas the visibility fallback. So a gate namingcurrent_userdid not hide the field from the people it named: it showed the field toeveryone, with no signal beyond one deduped
console.warn. Fail-open in the dangerousdirection — which is why this is graded a Bug and not an enhancement.
This restores what two accepted ADRs already declared:
record+current_user"Predicates that silently failed open on form fields and sections now evaluate, and a
rule resolving false now hides a field that is visible today. A host who authored
a
visibleWhennamingcurrent_user, saw the field render, and concluded the rule waspermissive was looking at a broken rule, not a permissive one — and that field will now
hide. The changeset states this in those terms and is marked
minoron@object-ui/components.Two things deliberately do not change: a genuinely unbound root still fails open (only
evaluated-and-false hides), and
visibleWhenremains presentation rather than accesscontrol.
The pin is the deliverable
packages/components/src/renderers/form/__tests__/predicate-scope-parity-6010.test.tsxruns one authored predicate text through five binding surfaces in three modes.
The defect exists precisely because two surfaces got the scope and one did not, so no
single-surface assertion can express the contract.
visibleWhenvisibleWhen(select options)visibleWhenvisibleWhenvisibleOn(ADR-0089 D2 alias)evaluated" is otherwise satisfiable by hiding everything, a worse bug than fail-open and
invisible to the denied column alone.
evaluated-and-false rather than could not be evaluated.
Reverse verification, direction predicted before running
Reverting the
scopeargument on one of the three call sites (the render-path one inrenderFormField) was predicted to turn red exactly the denied rows for form SECTIONand form FIELD, in the shown direction, leaving every other row green. Measured:
2 failed | 13 passed (15), both failuresAssertionError: expected true to be false, onthose two rows. The mutation's landing site was printed rather than assumed (line 1990,
with two lines of context), both the injected marker and the removed text were grepped
separately, the restore ran under
trap … EXIT INT TERM, andgit diff HEAD --statwasempty afterwards.
A bounded in-place fix, declared
The two
visibleOnsites inform.tsxreceive the same scope. ADR-0089 D2 foldsvisibleOnintovisibleWhenat parse, so binding one scope for the canonical spellingand another for its deprecated alias would have reproduced this exact defect one spelling
over. Same defect class, same file, same gate family — pinned as its own row in the table
above.
The synthesised legacy
condition: { field, equals }predicate keepsundefined: it isgenerated from a structured object and can only ever name a
recorddot field reference,so there is no authoring path by which it could reference
current_user.One structural change, not a one-liner
const predicateScope = usePredicateScope()moved ~75 lines up, abovereadonlyFieldNames. The card's hypothesis — "pass thepredicateScopealready in hand at:1276to the three calls" — does not hold as written for the first call site: that memofactory runs synchronously during render, so a
predicateScopedeclared below it is inthe temporal dead zone there, and reading it would have been a
ReferenceErroron firstrender rather than a missing binding. The hook is still called unconditionally and exactly
once per render; only its position among this component's hooks moved.
predicateScopejoins the dependency arrays of both hooks that consume it, matching theprecedent already in this file for
resolveCascadingOptions.Gates
All measured on
5f7796c44(git rev-parse --short HEADat the time of the runs; thetype-check and vitest runs were made on the working tree that became this commit
byte-for-byte, the eslint run after it).
pnpm --filter @object-ui/components type-check> @object-ui/components@17.6.0 type-check→tsc --noEmit && tsc -p tsconfig.test.jsonTYPECHECK_EXIT=0pnpm exec vitest run packages/components/src/renderers/form packages/core/src/evaluator --maxWorkers=2Test Files 65 passed (65)·Tests 725 passed (725)VITEST_EXIT=0eslint .inpackages/componentserrors: 0, 908 warningsESLINT_PKG_AFTER_EXIT=0The dependency closure was built first (
pnpm --filter '@object-ui/components^...' build,BUILD_CLOSURE_EXIT=0) so the type-check reads rebuilt.d.tsrather than a stale orabsent one.
eslint was run over the whole package, not narrowed — 398 files, 0 errors.
form.tsxcarried 60 warnings before this change and 60 after, with an identical rule breakdown
(
no-explicit-any51,react-refresh/only-export-components7,no-unused-vars2),measured by writing the base content to the same path so config resolution is identical.
No
--no-inline-config.Line-reference delta from the card
Re-derived on this branch's merge-base (
53dc89db8) rather than taken from the card, whosereferences came from an objectstack checkout of unknown vintage:
form.tsx:12011201form.tsx:12371237form.tsx:12761276form.tsx:14281428form.tsx:19351945form.tsx:21292139fieldRules.ts:158158optionRules.ts:103103ExpressionProvider.tsx:59,7059,70The
truevisibility fallback inresolveFieldRuleStatewas confirmed in that function'sown source, so the card's severity framing stands rather than inverting.
Out of scope, not folded in
visibleWhengate found by census: one authored-node bypass, a second evaluator with an oppositedatabinding, and total silence on fault in production #5926 (emptyActionbypass, thepage:tabssecond evaluator, production silence onfault) names no site this PR touches. No overlap.
content/docs/protocol/objectui/layout-dsl.mdxbinding table — andcontent/docs/ui/views.mdx:433, which carries the same claim — describe today'sasymmetry and will be wrong in the other direction once this lands. Not touched here; the
PM seat owns that follow-up.
Generated by Claude Code