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:
- 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.
- 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
- 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.
If the binding can be sparse, guard both predicates per the rule on materializeDeclaredFields in @objectstack/objectql — has() 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.
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.
Blocked-by: objectstack-ai/objectui#5401
Status:
pm:blockedon objectstack-ai/objectui#5401, which carries the ruled fix (record:alertmust consume the declaredvisibleWhengate). ⛔ The follow-up here — moving these two predicates tovisibleWhenwithhas()guards — must not start before that lands and the objectui pin advances. Today it breaks both banners.Filed unassigned by the
domain:metadataseat while implementing #8990 (sessionsession_01NTKPDRoynY8i3HmdSFUxFj, PR #9166). Recording only — not claimed, not queued. Backlink: #8990, #8975."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
undefinedandundefined == 'in_review'is plainfalse— 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/disabledpredicates: 34 of them, all now accounted for. Re-measuring that census onorigin/main@3851f87f0turned 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:79visible: 'record.id == ctx.user.id && record.email_verified == false'examples/app-showcase/src/ui/pages/task-detail.page.ts:51visible: "record.status == 'in_review'"Both are
record:alertpage-block predicates, both readrecord.*with nohas()guard, and both bind the record the client fetched rather than a total one.sys-user.page.ts:79carried a{dialect:'cel'}envelope while onlytask-detail.page.ts:51was bare; that was wrong — it mis-attributed a count taken overdefineAction-normalized action predicates in the built artifact. Measured three ways (authored source, builtdist, 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:
record:alertblock 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.record:alertsurface 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-verificationjob.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
sys_userpage and the showcasetask-detailpage. — ✅ DONE 2026-08-20.If the binding can be sparse, guard both predicates per the rule on⛔ STRUCK — measured harmful.materializeDeclaredFieldsin@objectstack/objectql—has()alone for these two.has()throws on the legacy evaluator these predicates actually run on, and on a fail-soft surface a throw means VISIBLE.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:alertsilently ignores the declared, spec-normalizedvisibleWhengate that the sibling block on the same page honours —page.zod.ts:189declaresvisibleWhen: ExpressionInputSchema, whilepropertiesis an opaquez.record(z.string(), z.unknown())bag whosevisiblekey nothing normalizes. That isdeclared ≠ enforcedon 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.