Skip to content

finding: PageComponentSchema.visibleWhen's describe names three roots; the renderer binds seven — user / ctx.user / os.user / app / features are undocumented on that surface #11256

Description

@os-zhuang

Filed unassigned by the domain:engine seat while implementing #9167 round 2 (session session_01RfyXxZ2WPjcjhuXpiQQc3y, PR #11254). Observation, not a defect claim — nothing is broken; the describe is narrower than the binding. Not claimed, not queued.

What the spec says

packages/spec/src/ui/page.zod.ts:189:

Visibility predicate (CEL) — component rendered only when TRUE. Binds record, current_user, page.<var>.

What the renderer actually binds

SchemaRenderer (objectui packages/react/src/SchemaRenderer.tsx, pin 190fbd01d) builds the evaluator scope as the ambient predicate scope plus current_user, the bound record, data (the data-source adapter) and page. The ambient scope comes from app-shell/src/providers/ExpressionProvider.tsx:

constscope={current_user: user, user,ctx: { user },os: { user }, app, data, features };

with the comment that these are the deliberate ADR-0068 aliases, "so a predicate authored against any one form evaluates identically on client, server-formula, and server-RLS".

So a page-component visibleWhen binds record, current_user, user, ctx.user, os.user, app, features, page.* and data — nine roots where the describe names three.

Why it is worth recording rather than shrugging at

  1. The platform's own metadata uses an undocumented one. The sys_user alert gate ruled on finding: two authored record:alert page-block visible predicates read record.* unguarded — same sparse-face fault class as #8990, but outside its action-only census #9167 is has(record.id) && has(record.email_verified) && record.id == ctx.user.id && …, matching the sibling resend_verification_email action predicate on the same object. Measured working in a real console (verified user → hidden, unverified self → VISIBLE, another user → hidden), so ctx.user is genuinely bound here — but an author reading the describe would conclude it is not, and an author reading the platform page would conclude the describe is stale. Both readings are half right.
  2. data means two different things on two surfaces that share the key name. On a component node the evaluator binds data to the data-source adapter; page:tabs' item-level visibleWhen (objectui containers.tsx:449) builds its own evaluator and binds data to the record row. Neither is documented at page.zod.ts:189.
  3. The describe is what an AI author reads before writing a gate, which is exactly the axis finding: two authored record:alert page-block visible predicates read record.* unguarded — same sparse-face fault class as #8990, but outside its action-only census #9167 was bought on: the declared surface should be the one that works.

⚠️Not a request to widen anything. Contract-first says the fix could equally be narrowing the renderer to the declared three rather than documenting the other six — the aliases are ADR-0068's, and whether ADR-0068 was ever meant to reach the page-component surface is the question this card cannot answer from here. Whoever takes it should settle that first; the wrong move is to paste the renderer's list into the describe because it happens to be what the code does.

Adjacent, filed separately: #11255 (raw-literal pages serve ExpressionInputSchema keys un-normalized).


Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions