Skip to content

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

Description

@os-zhuang

Blocked-by: objectstack-ai/objectui#5401

⛔⛔ STOP — steps 2 and 3 of "What is owed" below are STRUCK. Do not execute them.

A browser round on 2026-08-20 measured them to break both banners. Applying has() to these two predicates takes a banner that is currently correctly hidden and makes it permanently visible: on a todo task the "Awaiting review" banner appears; on a verified user "Email not verified" appears next to the page's own chip reading "Email Verified: Yes". Screenshot-confirmed, both pages, in the real console at the pinned objectui SHA.

They are struck because they are the instruction a future agent would mechanically follow. Evidence: report comment on this issue, 2026-08-20. The rest of this body is the original filing and is preserved.

Status: pm:blocked on objectstack-ai/objectui#5401, which carries the ruled fix (record:alert must consume the declared visibleWhen gate). ⛔ The follow-up here — moving these two predicates to visibleWhen with has() guards — must not start before that lands and the objectui pin advances. Today it breaks both banners.

Filed unassigned by the domain:metadata seat while implementing #8990 (session session_01NTKPDRoynY8i3HmdSFUxFj, PR #9166). Recording only — not claimed, not queued. Backlink: #8990, #8975.

⚠️ The title's attribution is also falsified

"same sparse-face fault class as #8990" is not true of these two predicates. Measured: both are served as bare strings, so both run on objectui's legacy JS evaluator, where a missing key is undefined and undefined == 'in_review' is plain false — not a fault. The absent-key fault class does not reach this surface at all. What the card found is real; what it named it is wrong.

What the #8990 census did not cover

#8990's census counts action visible / disabled predicates: 34 of them, all now accounted for. Re-measuring that census on origin/main @ 3851f87f0 turned up two more authored predicates in the same fault class that are not actions, so no #8990 assertion reaches them and neither is migrated by PR #9166:

  • packages/platform-objects/src/pages/sys-user.page.ts:79
    visible: 'record.id == ctx.user.id && record.email_verified == false'
  • examples/app-showcase/src/ui/pages/task-detail.page.ts:51
    visible: "record.status == 'in_review'"

Both are record:alert page-block predicates, both read record.* with no has() guard, and both bind the record the client fetched rather than a total one.

⚠️ Both are bare strings. An earlier PM note claimed sys-user.page.ts:79 carried a {dialect:'cel'} envelope while only task-detail.page.ts:51 was bare; that was wrong — it mis-attributed a count taken over defineAction-normalized action predicates in the built artifact. Measured three ways (authored source, built dist, served /api/v1/meta/page) the two pages are identical on every dimension tested. ⛔ They are one case, not two, and that correction is not to be re-inherited.

Why this is an observation and not a defect claim

The severity depends on a fact this seat did not measure: what a page-block predicate actually binds, and what the renderer does when one faults. The two differ from the action face in the ways that matter:

  1. A record:alert block sits on a record-detail page, so its binding is a detail read rather than a list row. If a detail read always projects every declared column, the absent-key half of the fault class never fires here and only the projected-null half is live — and both predicates above are bare equalities, which CEL answers rather than faults on a null.
  2. The failure DIRECTION is unknown here and is the opposite of the action face on at least one renderer. The action face is fail-closed (the button vanishes). objectui#4807 records the record:alert surface as fail-soft — "fail-soft 使作者门控的横幅永远显示", i.e. an unevaluable predicate leaves the banner permanently VISIBLE.

So the right first step is a measurement, not a rewrite: boot the real app, open a record-detail page whose read does not project the gated column, and see what the banner does. That is a dogfood-verification job.

That measurement has now been done (2026-08-20, real console at pinned objectui 9a3daf8d, Playwright/chromium). Both hypotheses resolved: fail-soft confirmed by observation; the detail read does project every declared column (25/26 and 25/25, the only absent key being the __search companion objectql withholds by rule #7642); and the forced-sparse case does not fault — it evaluates to false and hides, because the predicate never reaches CEL.

Why it is filed rather than folded into #8990

#8990's card, its census, and its dispatched file surface are all scoped to action predicates, and PR #9166 stayed inside that scope deliberately. Extending #8990's scope after the fact would also silently invalidate its "34" figure, which several documents now cite.

What is owed, if it is taken up

  1. Measure the page-block predicate binding and the renderer's fault direction, on both the platform sys_user page and the showcase task-detail page. — ✅ DONE 2026-08-20.
  2. If the binding can be sparse, guard both predicates per the rule on materializeDeclaredFields in @objectstack/objectqlhas() alone for these two.STRUCK — measured harmful. has() throws on the legacy evaluator these predicates actually run on, and on a fail-soft surface a throw means VISIBLE.
  3. If the direction is fail-SOFT, the guard is not merely cosmetic: it changes a permanently-shown banner into a correctly-hidden one, and wants its own before/after.STRUCK — the polarity is backwards. The banners are currently correctly hidden in the sparse case; the guard is what makes them wrongly shown. The before/after was run and it runs the other way.

The real finding, and where it now lives

record:alert silently ignores the declared, spec-normalized visibleWhen gate that the sibling block on the same page honours — page.zod.ts:189 declares visibleWhen: ExpressionInputSchema, while properties is an opaque z.record(z.string(), z.unknown()) bag whose visible key nothing normalizes. That is declared ≠ enforced on the objectui side, and it is the same pair of defects already closed on the record-header face (objectui#3521, #4213, #4419). Ruled 2026-08-20 and filed as objectstack-ai/objectui#5401.

No lint rule: #8881 / PR #8979 declined the mirror gate on measured grounds and that verdict covers this surface too — sparseness is a property of the view's projection and of row data, not of the metadata a linter sees.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions