Skip to content

A page authored as a raw Page object literal serves every ExpressionInputSchema key BARE, and the console silently routes bare predicates to its legacy evaluator #11255

Description

@os-zhuang

Filed unassigned by the domain:engine seat while implementing #9167 round 2 (session session_01RfyXxZ2WPjcjhuXpiQQc3y, PR #11254). Recording the class; the one live instance is fixed in that PR. Not claimed, not queued.

The shape

PageComponentSchema.visibleWhen is an ExpressionInputSchema (packages/spec/src/ui/page.zod.ts:189), whose transform normalizes a bare string into the canonical { dialect: 'cel', source } envelope. That transform only runs if something parses the page. A page authored as

exportconstSysUserDetailPage: Page={}

is a plain typed object literal — the type is checked, nothing is parsed — so whatever the author wrote reaches /api/v1/meta/page verbatim. A page authored through definePage({ … }) is parsed and serves the envelope.

Measured on origin/main @ 824a996c0 with a real dev server, the two authored record:alert gates side by side after the #9167 migration text was applied literally to both:

pageauthoring formvisibleWhen on the wire
showcase_task_detail (definePage)parsed{"dialect":"cel","source":"has(record.status) && …"}
sys_user_detail (raw Page literal)not parsed"has(record.id) && …"bare

Why bare is not merely cosmetic

objectui's ExpressionEvaluator.evaluateCondition at the pinned SHA (190fbd01d) routes by shape, deliberately:

Bare strings and ${…} templates stay on the legacy path (back-compat deprecation window); only an explicit { dialect: 'cel' } envelope is rerouted.

The legacy JS evaluator has no has(), and the component-visibility surface is fail-soft, so a guarded predicate throws and resolves to SHOWN. Measured in the real console, on the bare form:

runprobeverdictexpected
B2sys_user self, email_verified forced trueVISIBLEhidden ❌
B3another user's profile (control: that page's own title)VISIBLEhidden ❌

Isolated to the envelope alone — one page, one predicate source, one rewrite path:

runprobeverdict
P1node visibleWhen rewritten to the bare stringVISIBLE
P2node visibleWhen rewritten to the envelope, same sourcehidden ✅

And in a production console bundle the fault is completely silent: SchemaRenderer's evaluateVisibilityPredicate short-circuits on if (!__DEV__) before the diagnostic probe, so nothing is logged. The whole failure is "a declared gate stops gating, with no output anywhere".

Current blast radius — one live instance, already fixed; the rest is latent

Swept over every *.page.ts in the repo (export form classified from code with comments stripped):

pageformvisibleWhen
packages/platform-objects/src/pages/sys-user.page.tsraw literal1 — fixed in #11254 via P
packages/platform-objects/src/pages/sys-organization.page.tsraw literal0
packages/platform-objects/src/pages/sys-position.page.tsraw literal0
packages/qa/downstream-contract/src/welcome.page.tsraw literal0
every page under examples/**definePage()7, all normalized

So today nothing else is broken. What makes this worth a card is that the hazard is invisible at authoring time: the type accepts the bare string, tsc is green, every test is green, the gate farm is green, and the only symptom is a banner that stops gating in a shipped platform page. The next predicate added to a raw-literal platform page reproduces it exactly.

Options, not a recommendation to implement blind

  • A. A gate. Assert that every page reachable from a platform package serves canonical envelopes for its ExpressionInputSchema keys — e.g. parse each exported Page through PageSchema in a test and compare, which also closes the general "raw literal was never parsed" hole rather than the predicate half of it.
  • B. Route platform pages through definePage(). Removes the fork at the source, but it changes the export shape of shipped platform pages and a strict parse may reject unrelated pre-existing looseness — needs its own measurement before anyone commits to it.
  • C. Author-side only (what Move both record:alert gates onto the has()-guarded node visibleWhen, as a CEL envelope (#9167) #11254 did): use P / an explicit envelope on raw-literal pages. Correct per instance, but nothing stops the next one.

Related, upstream and separate: the renderer's bare-string back-compat window is objectui's, and closing it there would change verdicts for every un-normalized predicate in the wild — not something to ask for from this side.


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