Uh oh!
There was an error while loading. Please reload this page.
Move both record:alert gates onto the has()-guarded node visibleWhen, as a CEL envelope (#9167) - #11254
Merged
Merged
Conversation
…onto the has()-guarded node visibleWhen (#9167) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
…e so has() actually runs (#9167) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
Contributor
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 2 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
This was referenced Aug 23, 2026
os-zhuang
marked this pull request as ready for review
August 23, 2026 07:05
Uh oh!
There was an error while loading. Please reload this page.
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#9167
Round 2 of #9167 — the implementation the 2026-08-23 measurement round and the PM ruling below it produced. Steps 2–3 of the card's "What is owed" stay retired: they name
properties.visible, whose evaluator has nohas()and never will. This is the replacement remedy.What changed
On both
record:alertpages, the predicate moves offproperties.visibleonto the component-nodevisibleWhen,has()-guarded, with thevisiblekey deleted in the same edit:examples/app-showcase/src/ui/pages/task-detail.page.tsvisibleWhen: "has(record.status) && record.status == 'in_review'"packages/platform-objects/src/pages/sys-user.page.tsvisibleWhen: P`has(record.id) && has(record.email_verified) && record.id == ctx.user.id && record.email_verified == false`Both ruled conditions hold in the landed diff, and both are verifiable on the wire rather than only in the source: across the 34 pages this app serves, the two
record:alertnodes carry['properties','type','visibleWhen']and noproperties.visible; repo-wide,properties.visiblenow appears on 0 served nodes.Pon the platform page is not decoration — the first draft of it shipped the exact defect this card exists to preventThe ruled string was authored literally, as a bare string, on both pages. Measured, that broke the
sys_userbanner outright — and the wire said why before the browser did:visibleWhenon the wireshowcase_task_detaildefinePage({…}){"dialect":"cel","source":"has(record.status) && …"}sys_user_detailPageobject literal"has(record.id) && …"— bareSysUserDetailPageis a raw typed object literal, so nothing runsExpressionInputSchema's transform over it and whatever is authored reaches the wire verbatim. And the console keeps bare strings on the legacy JS evaluator by design —ExpressionEvaluator.evaluateCondition, at the pin: "Bare strings and${…}templates stay on the legacy path (back-compat deprecation window); only an explicit{ dialect: 'cel' }envelope is rerouted." That evaluator has nohas(), the surface is fail-soft, so the gate stopped gating.Measured on the bare-string draft, in the real console:
sys_userself,email_verifiedforcedtrue(15 keys set)Ada Auditor)B3 is the decisive one: on someone else's profile
record.id == ctx.user.idis plainly false, and the banner showed anyway — the predicate was not being consulted at all.Isolated to the dialect envelope alone, on one page, one predicate, one rewrite path (
metaPatched=1in both):todotask, nodevisibleWhenrewritten to the bare stringtodotask, nodevisibleWhenrewritten to the envelope, same sourceP(@objectstack/spec/shared) emits the{ dialect: 'cel', source }envelope. After the fix, 8 of 8 servedvisibleWhenvalues are envelopes and 0 are bare.After-state, measured in a real console
Framework built from this branch (
Tasks: 71 successful, 71 total), console rebuilt from the repo's pin (✓ @objectstack/console dist ready (49388 KB) from objectui@190fbd01d061, bundle canary + spec-injection checks passed),objectstack dev --ui --seed-admin -p 39167, Playwright 1.62.1 drivingchromium-1194fromPLAYWRIGHT_BROWSERS_PATHvia anexecutablePathoverride (⛔playwright installnot run). Every mutation is a network-response rewrite against the shipped bundle; the authored source is only ever what the server already serves.Both banners, both polarities, all correct
in_reviewtodosys_userself,email_verified=falsesys_userself,email_verifiedforcedtrueThe guard actually holds — and is load-bearing
in_review,statusstripped from the read (6 keys removed), landed guarded gatesys_userself,email_verifiedstripped (2 keys removed), landed guarded gateG1↔G2 and G3↔G4 are the same page, the same record, the same strip — only the
has()guard differs. That is condition (b) measured on the landed gate rather than inherited.No flash during load
todotask, landed gate, polled every 150 ms for 8 s (52 samples)in_reviewtask, same pollThe census the measurement round could not do
The round-1 confidence gap: whether any other page-block renderer also ignores or mis-binds
visibleWhen. Read at the pin, then cross-checked in the browser.Finding:
visibleWhenis not a per-block concern at all. It is enforced once, generically, inpackages/react/src/SchemaRenderer.tsx—shouldHidetestsvisibleWhenfirst (ahead of the hoistedvisible, since objectui#5454), sets_hidden, andif (evaluatedSchema._hidden) return nullfires before the registry dispatches to any renderer. A block renderer cannot ignore the gate: it never sees the node. Every path that places an authored page-component node reaches it —RegionContentandFlatContent(renderers/layout/page.tsx), the slot→region synthesizer for record pages (plugin-detail/src/synth/buildDefaultPageSchema.ts, which foldsslots.alerts/highlights/details/tabs/discussionintocomponents), and everypage:*container's children viarenderChildren→SchemaRenderer.toRenderableSchemais a pass-through, so no key is lost on the way.Cross-checked behaviourally on block types this card does not touch, each with a paired mount:
record:path(showcase task page){cel,'false'}/{cel,'true'}record:highlights(showcase task page){cel,'false'}/{cel,'true'}record:highlights(platformsys_userpage){cel,'false'}/{cel,'true'}⛔ Nothing was migrated on the strength of this census — a wider migration is a separate card.
What the census did turn up, reported and not acted on:
renderers/complex/data-table.tsx:1974renders theemptyActionslot by resolving the registry directly — it callsComponentRegistry.get(node.type)and renders the returned component with the node as itsschemaprop, never throughSchemaRenderer— so avisibleWhenon that node is never evaluated. It is the only such bypass for authored nodes in the tree; the other direct-registry render (action-bar.tsx:300) is the ADR-0089 action face, which gates onvisibleinsideaction-button/action-iconby design.page:tabsitem-levelvisibleWhenis a second evaluator, with a wider binding and no diagnostic.containers.tsx:449builds its ownExpressionEvaluatorthat spreads the row flat and bindsdatato the row, where the node-level gate bindsdatato the data-source adapter — same key, opposite meanings — and it callsevaluateConditionwithout the devthrowOnErrorprobe, so a faulting tab predicate is silent where a faulting node predicate warns.evaluateVisibilityPredicateshort-circuits onif (!__DEV__), so the bare-string breakage above produced no console line at all — unlike theproperties.visibleface, which logs fromrecord-alert.tsx'suseConditionpath regardless. Round 1's"has" is not a functionlines came from that other face.page.zod.ts:189namesrecord,current_userand the page-variable root;app-shell/src/providers/ExpressionProvider.tsxputs{ current_user, user, ctx: { user }, os: { user }, app, data, features }into the scope — the ADR-0068 aliases.ctx.user.idin the ruled predicate is therefore a first-class binding, matching the siblingresend_verification_emailaction predicate on the same object, and measured working (B1/B2/B3).Verification
pnpm --filter @objectstack/platform-objects --filter @objectstack/example-showcase run test→Test Files 27 passed (27)/Tests 432 passed (432)andTest Files 25 passed (25)/Tests 367 passed (367).run typecheck→packages/platform-objects typecheck: Done,examples/app-showcase typecheck: Done.node scripts/pm/dispatch-gates.mjs(no paths passed — it derives the change set itself) named 15 path-matched families plus the convention-triggeredpnpm check:i18n. All 16 run, pluspnpm check:nul-bytes; all exit 0, verdict lines quoted in the report on the card.pnpm lint(repo-wide,eslint . --no-inline-config) reports89 errors, all 89 inside.cache/objectui-190fbd01d061/— the objectui clonepnpm objectui:buildcreates locally, gitignored at.gitignore:42and absent from a CI checkout. Measured rather than argued:pnpm exec eslint . --no-inline-config --ignore-pattern '.cache/**'exits 0 with no output.3d812516a.Notes for review
definePage()'s normalization rather than given an explicitP. That transform is the contract, and it is measured working here (its node serves the envelope, A1/A2 correct). The platform page needsPprecisely because it is not built that way.false.Generated by Claude Code