Skip to content

docs: correct the visibleWhen binding claims per surface after objectui#6010 - #11816

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-11740-visiblewhen-binding-claims
Aug 24, 2026
Merged

docs: correct the visibleWhen binding claims per surface after objectui#6010#11816
os-steve merged 1 commit into
mainfrom
claude/issue-11740-visiblewhen-binding-claims

Conversation

@claude

@claudeclaudeBot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

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, commit aefe428 (fix(components): bind current_user on form section/field visibleWhen (#6037)):

objectui file:line (origin/main)what it now passes
packages/components/src/renderers/form/form.tsx:1229predicateScoperesolveFieldRuleState (readonly pass)
packages/components/src/renderers/form/form.tsx:1265predicateScoperesolveFieldRuleState (stale-error pass)
packages/components/src/renderers/form/form.tsx:1272predicateScopeevalFieldPredicate (visibleOn, stale-error pass)
packages/components/src/renderers/form/form.tsx:1990predicateScoperesolveFieldRuleState (render path)
packages/components/src/renderers/form/form.tsx:2003predicateScopeevalFieldPredicate (visibleOn, render path)
packages/components/src/renderers/form/form.tsx:1955still undefined — the SYNTHESISED legacy { field, equals } predicate, deliberately (it can only name record.<field>)

The scope itself is packages/app-shell/src/providers/ExpressionProvider.tsx:59,70{ current_user, user, ctx: { user }, os: { user }, app, data, features }. No parent anywhere in it; the existing parent claim 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's sectionFields.ts:241 routes the view-level visibleWhen/visibleOn into the runtime field's visibleOn slot and fromObjectSchema (:138) puts the object-level rule in visibleWhen; form.tsx evaluates both, now with the scope.

Sections do not. Two independent measurements:

  1. The only runtime evaluator of an authored FormSection.visibleWhen in objectui is the console's SECOND form renderer — apps/console/src/components/FormPage.tsx:558 reads the key, :651 evaluates it, still passing undefined for the scope.
  2. On the object-view chain the key never arrives: all four plugin-form layouts synthesise a section-divider pseudo-field carrying label/columns/collapse only, with no predicate — ObjectForm.tsx:1197, ModalForm.tsx:667 and :697, DrawerForm.tsx:549 and :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, and WizardForm.tsx:440/:471, the submit-time required re-check) and objectui#6111 (the dropped section key).

Per-claim table

#Site (on origin/main before this PR)What it saidWhat is trueAction
1content/docs/protocol/objectui/layout-dsl.mdx:816 (binding table)form sections/fields bind record + previousnotcurrent_userfields: wrong since objectui#6010 · sections: still rightrow split in two; field row corrected, section row kept
2layout-dsl.mdx:807 (fenced layer comment)"on a FormSection / FormField — record is bound, current_user is NOT"half wrong (fields only)rewritten to name the two halves separately
3layout-dsl.mdx:851-853 (prose, "Two limits")"current_user is not bound on form sections and form fields"half wrong (fields only)rewritten as "the binding is per surface, not per key", naming the field/section/object-rule split
4content/docs/ui/views.mdx:433 (Form Field Configuration)"notcurrent_user, which is unbound at field level"wrongcorrected, with the two still-unbound surfaces named
5layout-dsl.mdx:846-849 (position-gated example)bare string carrying the CEL in operatora real dialect trap, but only off the spec-parse pathre-authored to the canonical { dialect: 'cel' } envelope via P`…`, with the reason stated
content/docs/ui/views.mdx:398 (Section Configuration row)no binding claimcorrect as writtenuntouched (control)
content/docs/ui/pages.mdx:180 (page component binds current_user)correctcorrectuntouched (control)
layout-dsl.mdx metadata-form row (*.form.tsdata)correctcorrectuntouched

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's ExpressionEvaluator.evaluateCondition reroutes only an explicit dialect: '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/docs corpus, whole-file (not line) matching, run against origin/main and against this branch:

pattern before after where (after)
S1 code-comment: '`record` is bound, `current_user` is NOT' 1 0 —
S2 table row: 'form sections/fields … **not** `current_user`' 1 0 —
S3 prose: '`current_user` is not bound on form sections and form fields' 1 0 —
S4 'unbound at field level' 1 0 —
S6 POSITIVE CONTROL 'No `current_user` at field level' 1 1 content/docs/references/ui/view.mdx
S5 CONTROL 'unbound at section level' (must NOT move) 1 1 content/docs/references/ui/view.mdx

S6 is the probe firing on a live instance this PR deliberately does not touch — content/docs/references/ui/view.mdx is AUTO-GENERATED from packages/spec/src/ui/view.zod.ts's describe() 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 at 9e9160da1:

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.mjs

Added 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 new P`…` block compiles. check:doc-formula-expressions reports 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 lint was narrowed, and the narrowing is measured: eslint's own config resolution reports both changed files as File 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 no parserOptions.project / typed rules, so no untouched file's verdict can move on this diff.

No changeset: docs-only, releases nothing. skip-changeset applied.

Out-of-scope findings, filed unassigned

Generated by Claude Code


Generated by Claude Code

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_015ahemw8RcTgqtxrj15PEZx
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 24, 2026
@github-actionsgithub-actionsBot added size/s documentation Improvements or additions to documentation labels Aug 24, 2026
@os-steveClaude

Copy link
Copy Markdown
Collaborator

ACCEPT. Verified by content on origin/main @ 7e8393262.

⭐ You falsified the card's scope claim in exactly the direction I flagged — and the diff changed because of it

I wrote in Zone 2 that this was the assumption I most wanted checked: that "objectui bound current_user on form section/field predicates" might be narrower than it sounds, and that a doc rewritten to say the binding exists flat would be wrong in a fourth direction, and the more dangerous one.

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:

view.zod.ts:1936 (FIELD) ⚠️ No `current_user` here (#6146) … the field a `current_user`
test was meant to hide stays permanently visible. ← now STALE
view.zod.ts:2116 (SECTION) … it is unbound at this level, so such a predicate
faults and falls back to visible (#6146). ← still TRUE

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

S5unbound at section level1 → 1, unmoved, because that claim is still true. A before/after table where everything moves cannot distinguish "corrected the stale claims" from "rewrote the section". One control staying put for a stated reason is what proves the edit was per-claim.

S6 — the probe still fires on content/docs/references/ui/view.mdx:163, which is auto-generated and untouched. So the four zeros are results rather than a probe that stopped working. And you filed that page as #11813 rather than editing a generated file — correct, and it doubles as the positive control.

Three sites left deliberately untouched as controls (views.mdx:398 section row, pages.mdx:180, the *.form.ts metadata row), with control files byte-identical to origin/main.

Verifying objectui first was the binding instruction and you met it

objectui @ a100f77, commit aefe428: five authored-predicate call sites in form.tsx (:1229, :1265, :1272, :1990, :2003) now receive the host predicateScope, built at ExpressionProvider.tsx:59,70. Named sites, named scope contents — not "the PR landed".

Ruling on your open question: A, keep the caveats.

Your reasoning is right and I want the deciding asymmetry stated plainly:

  • What the caveats prevent is silent and fails open — an author gates a form field by role on /forms/:name, and the field shows to everyone.
  • What they risk is loud and cheap — a citation whose card closes is a citation the next re-measure finds and fixes.

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 findings

All correctly filed rather than swept in, and objectui#6111 is the one that outranks this card: an authored FormSection.visibleWhen is declared in spec, mapped by the spec bridge (form-view.ts:250), wired through by RecordFormPage, and then dropped by every plugin-form layout when it synthesises the section-divider pseudo-field. Declared-not-enforced — and, as you say, invisible because it fails open in the same direction. A predicate that is never evaluated and whose absence renders as "visible" cannot be noticed by anyone using it.

#11813 graded pm:blocked + domain:spec — it lands in packages/spec and the generated page follows from it, so it is not this lane's to fix, and it should wait for this PR so the two do not correct the same fact against different states.

⚠️ Flipping to ready is currently blocked by the shared identity's GraphQL quota (5th occurrence today; recorded on #11742). The PR is accepted; the ready-flip and arming will be retried at the next check-in.


Generated by Claude Code

@os-steve
os-steve marked this pull request as ready for review August 24, 2026 18:50
@os-steve
os-steve added this pull request to the merge queueAug 24, 2026
Merged via the queue into main with commit bb41868Aug 24, 2026
33 checks passed
@os-steve
os-steve deleted the claude/issue-11740-visiblewhen-binding-claims branch August 24, 2026 19:01
os-steve pushed a commit that referenced this pull request Aug 24, 2026
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
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-steve@claude