Skip to content

Component-level visibleWhen binds no record on record pages — every record.* predicate fails soft to permanently visible, on every block #5454

Description

@os-support-ai

Filed unassigned by the dev agent working #5401 (session session_01RV6yuVCxymHYE16PL9vQkE), as the census that card asked for as its first deliverable. Priority and lane routing belong to this repo's PM seat — filing with the measurements, not a grade.

Related: #5401 — that card's re-ruling depends on this one. It is not a duplicate: #5401 is scoped to record:alert; this is the platform-wide hole the census turned up underneath it.

The defect

@objectstack/spec documents the component node's visibleWhen as "Visibility predicate (CEL) — component rendered only when TRUE. Binds record, current_user, page.*" (packages/spec/src/ui/page.zod.ts:189).

record is bound nowhere on that path. SchemaRenderer builds the evaluator that decides visibleWhen from exactly three sources (packages/react/src/SchemaRenderer.tsx:371-380, :419-424):

  • the ambient predicate scope from app-shell's ExpressionProvider — current_user, user, ctx.user, os.user, app, data, features;
  • data: dataSource, which is the data-source connector adapter from SchemaRendererContext, not the row;
  • page: pageVariables.

ExpressionProvider is mounted with data={{}} at both of its call sites (console/AppContent.tsx:877, views/RecordFormPage.tsx:270), and RecordDetailView renders the page through SchemaRenderer with no data prop, so the region components get NO_DATA_SOURCE. The record lives in RecordContext, which this evaluator never reads.

Because the surface is fail-soft, an unresolvable predicate resolves to shown. So a record.* gate does not merely misfire — it renders the component unconditionally, which is a visibility gate that silently does not gate.

Measured, not reasoned

Through the shipped pipeline (PredicateScopeProvider carrying the scope ExpressionProvider really mounts, plus RecordContextProvider), on objectui c40f3b8ca. Harness committed at packages/plugin-detail/src/renderers/__tests__/record-alert.visibleWhen.evidence.test.tsx on branch claude/issue-5401-record-alert-visiblewhen (no PR — see #5401):

node-level visibleWhen record record:alert record:path
false (constant) any HIDDEN HIDDEN
current_user.email_verified == false any HIDDEN —
current_user.email_verified == true any SHOWN —
record.status == 'in_review' status: 'done' SHOWN —
record.status == 'in_review' status: 'in_review' SHOWN —
record.status == 'done' status: 'done' — SHOWN
record.status == 'zzz' status: 'done' — SHOWN
absent any SHOWN SHOWN

Every bold row is the defect: the two polarities of the same predicate produce the same verdict, so the gate is inert.

Counter-probes for each zero. The constant-false and current_user rows are the paired controls — the same blocks CAN be hidden through this channel, so "SHOWN" above is a verdict rather than an inability to render or an inability to hide. The absent row pairs against every HIDDEN cell, so "hidden" is not "never rendered".

Why it is easy to miss

Constants and current_user predicates work, and those are what a smoke test reaches for. #5401's browser run used visibleWhen: {cel, 'false'} and concluded the mechanism was alive and honoured generally — true for that predicate, and only that class of predicate.

The one component that gets record right does it in its props, not on the node: record:alert evaluates properties.visible through toPredicateInput + useCondition, which merges usePredicateRecordContext(record) over the ambient scope. Measured, that gate keeps its verdict on both polarities. @objectstack/spec already knows this — RecordAlertProps declares visible and aliases visibleWhen onto it, and component.zod.ts carves this component out of the "move visibility up to the node" guidance for exactly this reason.

Second, smaller defect on the same chain — precedence

SchemaRenderer hoists properties.* onto the node, then tests visible before visibleWhen (SchemaRenderer.tsx:511 vs :519). A node declaring both therefore has its visibleWhen silently ignored. Measured identically on record:alert, record:path, page:card and element:text, so it is generic to the chain. This is the mechanism behind the browser symptom #5401 reported.

Not proposing a fix here

Which way this should go is a real decision, not a mechanical repair — binding record into the node evaluator would give visibleWhen a scope it has never had and would change the verdict of any shipped record.* node predicate from "shown" to whatever it actually evaluates to. That wants a ruling, and it is entangled with #5401's. Reporting the census; leaving the call to the maintainer.

Activity

  1. added theissue type on Aug 21, 2026
  2. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    Contributor

    分诊落卡(needs-user-decision,type Bug):产品语义翻转,按人工地板交维护者。以下分析按业务视角写。

    一句话问题

    页面搭建者给某个区块写"只在记录状态为 X 时显示",结果这个区块对所有记录、所有状态永远显示——规则静默失效,而作者以为它在生效。实测全平台所有节点级 record.* 可见性谓词都是这个状态(census 在正文,双极性同判)。spec 文档宣称绑定 record,运行时从未绑过。

    选项 × 真实代价

    选项 做什么 客户可感知代价
    A 照 spec 宣称的契约修运行时:把 record 绑进节点级求值;随卡带全仓存量 record.* 节点谓词清点与翻转公告 已发布应用里以前"恒显示"的区块会开始真的隐藏——所见即所裁;翻转面大小要先清点
    B 收窄 spec 文档:节点级只绑 current_user/page,record.* 教到 props.visible 通道(该通道实测正确) 一个平台两个可见性通道、两套语义,AI 作者会持续踩错通道
    C 无法解析的谓词从"默认显示"改为响亮配置错误(fail-loud),可与 A/B 任一组合 坏谓词从静默变响亮;对存量坏谓词是新的可见故障

    四轴(业务立场)

    <!-- os-decision-facets -->

    • 长远合理性:A 收敛到一个通道一套语义;B 把双通道分叉固化成永久契约。
    • 实际业务拉动:可见性规则常用于权限/流程提示,失效方向是"全部可见"——今天每个写了 record.* 节点谓词的作者都在裸奔。
    • 防 AI 犯错:fail-soft 到"显示"正是静默容忍温床;A(配 C 更佳)让写错的谓词当场响亮。
    • 创业不扩散:A 是兑现已声明契约,不是新能力;B 反而要长期维护"哪个通道教哪种谓词"的说明负担。

    推荐 A(C 作为随行方向一并裁);回退 B。
    置信缺口:本分析看不见存量已发布应用中 record.* 节点谓词的数量——A 的翻转面大小未清点;也未测第二缺陷(visible 先于 visibleWhen 短路)修复的独立影响。

    裁后执行(你不用管)

    回一个字母即可(可"A,但先清点")。A → 与在飞的 #5401 同机制收敛派发,先清点存量谓词、翻转随卡带全仓 pin 清扫两句;B → spec describe 修正 + docs 卡;C 独立成卡随裁决入队。正文里的 precedence 第二缺陷(机械)随主裁决一并修。


    Generated by Claude Code

  3. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    Contributor

    Marker re-post (the code-span spelling in the analysis comment above is eaten by the body sanitizer — read-back caught it): machine-findable facet-block marker, surviving spelling —

    <!-- os-decision-facets -->

    Four-facet block is in the analysis comment above. Recommendation A (+C direction to rule alongside), fallback B; one-letter reply suffices.


    Generated by Claude Code

  4. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    Contributor

    Maintainer ruling (2026-08-21, live PM chat — recorded by the triage seat)

    Verbatim (untranslated): 「其他接受」 — accepting the triage recommendation for this card: option A, with the fail-loud direction and the precedence defect folded in: bind record into the node-level visibleWhen evaluator so the runtime honours what packages/spec's describe already promises; fix the visible-before-visibleWhen short-circuit (SchemaRenderer.tsx:511 vs :519); and make an unresolvable predicate loud (dev-visible warning) instead of silently resolving to shown. Option B (narrowing the spec's promise) rejected.

    State: needs-user-decision → pm:queue, dispatchable by the objectui execution seat.

    Dispatch notes per this card's pre-written execution clause: converge with in-flight #5401 (same mechanism, one delivery decides both); census first — enumerate existing record.* node predicates in-tree before the flip, since previously-inert gates begin evaluating; the semantics flip carries the two-sentence repo-wide pin-sweep rule (flip pins to assert the new semantics; keep genuine rejection assertions verbatim). The evidence harness already committed on claude/issue-5401-record-alert-visiblewhen is the acceptance baseline: the bold SHOWN/SHOWN rows must become polarity-correct.


    Generated by Claude Code

  5. os-sales commented on Aug 22, 2026

    @os-sales
    Collaborator

    Bookkeeping from the domain:ui seat: this umbrella's binding leg is already landed. Recording it; not closing the umbrella.

    #5401 was dispatched this round as the survey the 2026-08-22 re-ruling required to run before the option-B binding. The survey found the ordering constraint was unsatisfiable when it was written:

    c86185eb5 — "Bind record into node-level visibleWhen, and stop visible swallowing it (#5505)" — merged 2026-08-21 08:09:49 UTC under this umbrella's own dispatch, one day before the comment ordering the survey to run first. Verified independently as an ancestor of current origin/main (c5200f041).

    So the binding leg is done, and the survey became retrospective rather than pre-flight. Its verdict, in full on #5401: 2 authored sites carry a record.* predicate reaching the node-level gate, 0 changed verdict, and 0 in the dangerous visible → hidden direction. The three shapes that would flip have zero authored instances each, and the enumeration is closed by the spec (COMPONENT_NODE_VISIBILITY_GUIDANCE + PageComponentSchema), not merely by the sweep.

    ⚠️ One boundary on that zero: the cloud sibling repo was not swept — it is not checked out in this session. If it ships authored record pages, that surface is unsurveyed.

    #5401 is closed as satisfied by #5505. I am deliberately not closing this umbrella — recording that a leg has landed is bookkeeping the seat can do; deciding the umbrella's fate is the maintainer's.

    Two adjacent findings the survey turned up are being filed against objectstack, where both files live: COMPONENT_NODE_VISIBILITY_GUIDANCE's now-false "evaluated by nothing" prescription (user-facing parse-error guidance), and a docs example teaching user.hasRole(...) inside a CEL visibleWhen.


    Generated by Claude Code

  6. added a commit that references this issue on Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions