Uh oh!
There was an error while loading. Please reload this page.
docs: correct the visibleWhen binding claims per surface after objectui#6010 - #11816
Conversation
objectui#6010 bound the host predicate scope on the five authored-predicate
call sites in objectui's form renderer, so `current_user` (plus the ADR-0068
`user` / `ctx.user` / `os.user` aliases) now resolves on a runtime form FIELD
`visibleWhen` / `visibleOn` the way it already did on the page/app-nav node gate
and per-option `visibleWhen`. The two hand-written pages that documented the
old asymmetry are corrected to the measured binding.
Measured per claim rather than swept: the FIELD half moved, the SECTION half
did not. `FormSection.visibleWhen` is read by exactly one runtime evaluator in
objectui — the console's second form renderer — which still passes `undefined`
for the scope, and the object-view chain (ObjectForm / ModalForm / DrawerForm /
SplitForm) drops the key onto a `section-divider` pseudo-field that carries no
predicate at all. So the binding table's one form row becomes two, and the
section row keeps the claim it still earns.
Two renderer caveats are stated where the binding is, because promising a
binding a live surface does not honour is the failure this page keeps hitting:
the console's standalone form routes (`/forms/:name`, public `/f/:slug`)
evaluate field predicates unbound (objectui#6110), and section predicates are
inert on the object-view chain (objectui#6111).
The position-gated example moves to the canonical `{ dialect: 'cel' }` envelope
via `P`. A bare string carrying the CEL membership operator is normalized by
spec parse for authored metadata, but a schema handed straight to the renderer
keeps the bare string, which objectui routes to its legacy evaluator — no `in`
operator, rejected, then fail-open (objectui#2661 keeps that routing).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZxos-steve
commented
Aug 24, 2026
ACCEPT. Verified by content on ⭐ You falsified the card's scope claim in exactly the direction I flagged — and the diff changed because of itI wrote in Zone 2 that this was the assumption I most wanted checked: that "objectui bound It was narrower. The field half is now bound; the section half is still unbound. So you split the binding table's one form row instead of sweeping it. Verified at the schema, which is where it is unarguable: A uniform sweep would have written "bound" over both and produced a doc that promises a role gate works on sections where it silently does not — fail-open, on the security-shaped half. That is the outcome the split avoided. The controls are what make the split credible⭐ S5 — ⭐ S6 — the probe still fires on Three sites left deliberately untouched as controls ( Verifying objectui first was the binding instruction and you met it
Ruling on your open question: A, keep the caveats.Your reasoning is right and I want the deciding asymmetry stated plainly:
You do not trade a silent failure for a loud one. C is a reasonable compromise if the maintainer wants the contract table contract-only, but B I would not take while objectui#6110 is open, and neither would you. The three findingsAll correctly filed rather than swept in, and objectui#6111 is the one that outranks this card: an authored #11813 graded Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
One conflict, in `scripts/check-override-consistency.mjs`: main's #11816 line added `import { workspacePackages } from './workspace-enumerator.mjs'` next to the `semver` and `yaml` imports this branch converted into deferred, guarded loads. Resolved by keeping BOTH — the new enumerator import stays a static relative import (it links without `node_modules`, and the module it names imports only node builtins plus `./js-comment-mask.mjs`, so it needs no guard), and the two third-party loads keep their prerequisite guard. `scripts/check-step-collectors.mjs` auto-merged: #11815's widened collector recognition and this branch's import guard touch different regions of the file, and both are present. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
Fixes#11740
objectui#6010 (objectui PR #6037, merged 2026-08-24) bound the host predicate scope on the five authored-predicate call sites in objectui's form renderer. This corrects the two hand-written pages that documented the pre-#6010 asymmetry — per claim, not in bulk: the FIELD half of that claim is now wrong, the SECTION half is still right, and both are measured below rather than inferred from the card.
Upstream evidence — verified by content, in objectui, before a word was written
objectui
origin/main@a100f77, commitaefe428(fix(components): bind current_user on form section/field visibleWhen (#6037)):origin/main)packages/components/src/renderers/form/form.tsx:1229predicateScope→resolveFieldRuleState(readonly pass)packages/components/src/renderers/form/form.tsx:1265predicateScope→resolveFieldRuleState(stale-error pass)packages/components/src/renderers/form/form.tsx:1272predicateScope→evalFieldPredicate(visibleOn, stale-error pass)packages/components/src/renderers/form/form.tsx:1990predicateScope→resolveFieldRuleState(render path)packages/components/src/renderers/form/form.tsx:2003predicateScope→evalFieldPredicate(visibleOn, render path)packages/components/src/renderers/form/form.tsx:1955undefined— the SYNTHESISED legacy{ field, equals }predicate, deliberately (it can only namerecord.<field>)The scope itself is
packages/app-shell/src/providers/ExpressionProvider.tsx:59,70—{ current_user, user, ctx: { user }, os: { user }, app, data, features }. Noparentanywhere in it; the existingparentclaim in the docs was left untouched rather than widened or narrowed on a measurement this PR did not make.The scope of the binding is narrower than "runtime forms" — measured, and it changes the diff
Fields reach the bound call sites:
@object-ui/plugin-form'ssectionFields.ts:241routes the view-levelvisibleWhen/visibleOninto the runtime field'svisibleOnslot andfromObjectSchema(:138) puts the object-level rule invisibleWhen;form.tsxevaluates both, now with the scope.Sections do not. Two independent measurements:
FormSection.visibleWhenin objectui is the console's SECOND form renderer —apps/console/src/components/FormPage.tsx:558reads the key,:651evaluates it, still passingundefinedfor the scope.section-dividerpseudo-field carrying label/columns/collapse only, with no predicate —ObjectForm.tsx:1197,ModalForm.tsx:667and:697,DrawerForm.tsx:549and:605,SplitForm.tsx:341. The spec bridge does carry it that far (packages/react/src/spec-bridge/bridges/form-view.ts:250); it is dropped at the last hop.So the binding table's single form row becomes two, and the section row keeps the claim it still earns. Filed rather than fixed here: objectui#6110 (the two evaluators still passing
undefined— console FormPage on both routes, andWizardForm.tsx:440/:471, the submit-time required re-check) and objectui#6111 (the dropped section key).Per-claim table
origin/mainbefore this PR)content/docs/protocol/objectui/layout-dsl.mdx:816(binding table)record+previous— notcurrent_userlayout-dsl.mdx:807(fenced layer comment)recordis bound,current_useris NOT"layout-dsl.mdx:851-853(prose, "Two limits")current_useris not bound on form sections and form fields"content/docs/ui/views.mdx:433(Form Field Configuration)current_user, which is unbound at field level"layout-dsl.mdx:846-849(position-gated example)inoperator{ dialect: 'cel' }envelope viaP`…`, with the reason statedcontent/docs/ui/views.mdx:398(Section Configuration row)content/docs/ui/pages.mdx:180(page component bindscurrent_user)layout-dsl.mdxmetadata-form row (*.form.ts→data)On item 5, both halves were verified rather than taken from the card:
ExpressionInputSchema(packages/spec/src/shared/expression.zod.ts:99) normalizes a bare string to{ dialect: 'cel', source }, so authored metadata is unaffected either way; objectui'sExpressionEvaluator.evaluateConditionreroutes only an explicitdialect: 'cel'envelope to the CEL engine and leaves bare strings on the legacy evaluator (objectui#2661, quoted in that method's own comment). The example now teaches the envelope and says why.Non-vacuity, with controls
Literal probe over the whole
content/docscorpus, whole-file (not line) matching, run againstorigin/mainand against this branch:S6 is the probe firing on a live instance this PR deliberately does not touch —
content/docs/references/ui/view.mdxis AUTO-GENERATED frompackages/spec/src/ui/view.zod.ts'sdescribe()text, so the zeros above are results and not an inability to find anything. That generated page and its spec source are filed as #11813. S5 is the section-level claim, which is still true and must not move — and does not.What was verified, and what CI still owes
Gate families derived from the diff with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(17 matched), all run green at9e9160da1:check:cross-package-test-inputs(both spellings) ·check:doc-anchors·check:doc-authoring·@objectstack/lint check:doc-formula-expressions·@objectstack/lint check:doc-security-posture·check:docs-audit-scope·check:docs-redirects·@objectstack/spec check:empty-state·check:liveness·check:strictness-ledger·check:variant-docs·check:published-readme-links·check:react-page-adapter-contract·check:role-word·check-doc-frontmatter.mjs·check-section-landing-index.mjsAdded beyond the derived list because the diff edits an
{/* os:check */}block:@objectstack/spec check:skill-examples— "✅ 256 prose examples type-check across 3 surface(s)", which is what proves the newP`…`block compiles.check:doc-formula-expressionsreports the two edited fenced sites as SKIPPED (layer not statically determinable at statement position) exactly as it did before, and its judged population is unchanged: "14 predicate(s) on a statically determinable field layer judged clean; 6 skipped as undeterminable."Repo-wide
pnpm lintwas narrowed, and the narrowing is measured: eslint's own config resolution reports both changed files asFile ignored because no matching configuration was supplied(population is**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}), the count is read from--format json(2 files, 0 errors), and the config declares noparserOptions.project/ typed rules, so no untouched file's verdict can move on this diff.No changeset: docs-only, releases nothing.
skip-changesetapplied.Out-of-scope findings, filed unassigned
/forms/:name,/f/:slug) and the wizard's submit-time required re-check still passundefinedfor the predicate scope.FormSection.visibleWhenis dropped by all four plugin-form layouts: declared, bridged, then never evaluated on the object-view chain.packages/specstill tells authorscurrent_useris unbound on a form FIELDvisibleWhen— three text sites stale after objectui#6010 (and the section slot must NOT be swept with them) #11813 —packages/spec'sFormFieldSchema.visibleWhenJSDoc +describe()still saycurrent_useris unbound at field level (and the generatedcontent/docs/references/ui/view.mdx:163copies it, plus two lint-side comments). The SECTION slot there is still correct and must not be swept with it.Generated by Claude Code
Generated by Claude Code