Repository navigation
Component-level visibleWhen binds no record on record pages — every record.* predicate fails soft to permanently visible, on every block #5454
Description
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 fromSchemaRendererContext, 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
os-zhuang commentedon Aug 21, 2026
分诊落卡(
needs-user-decision,type Bug):产品语义翻转,按人工地板交维护者。以下分析按业务视角写。一句话问题
页面搭建者给某个区块写"只在记录状态为 X 时显示",结果这个区块对所有记录、所有状态永远显示——规则静默失效,而作者以为它在生效。实测全平台所有节点级
record.*可见性谓词都是这个状态(census 在正文,双极性同判)。spec 文档宣称绑定record,运行时从未绑过。选项 × 真实代价
record绑进节点级求值;随卡带全仓存量record.*节点谓词清点与翻转公告current_user/page,record.*教到props.visible通道(该通道实测正确)四轴(业务立场)
<!-- os-decision-facets -->record.*节点谓词的作者都在裸奔。推荐 A(C 作为随行方向一并裁);回退 B。
置信缺口:本分析看不见存量已发布应用中
record.*节点谓词的数量——A 的翻转面大小未清点;也未测第二缺陷(visible先于visibleWhen短路)修复的独立影响。裁后执行(你不用管)
回一个字母即可(可"A,但先清点")。A → 与在飞的 #5401 同机制收敛派发,先清点存量谓词、翻转随卡带全仓 pin 清扫两句;B → spec describe 修正 + docs 卡;C 独立成卡随裁决入队。正文里的 precedence 第二缺陷(机械)随主裁决一并修。
Generated by Claude Code
os-zhuang commentedon Aug 21, 2026
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
os-zhuang commentedon Aug 21, 2026
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
recordinto the node-levelvisibleWhenevaluator so the runtime honours whatpackages/spec's describe already promises; fix thevisible-before-visibleWhenshort-circuit (SchemaRenderer.tsx:511vs: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 onclaude/issue-5401-record-alert-visiblewhenis the acceptance baseline: the bold SHOWN/SHOWN rows must become polarity-correct.Generated by Claude Code
os-sales commentedon Aug 22, 2026
Bookkeeping from the
domain:uiseat: 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:
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.cloudsibling 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 teachinguser.hasRole(...)inside a CELvisibleWhen.Generated by Claude Code
fix(react): make a node-gate `data.*` predicate the adapter cannot an…
fix(react): report a `${…}` visibility predicate collapsed inside `pr…
Bind `record` into node-level `visibleWhen`, and stop `visible` swall…
fix(react): make a node-gate `data.*` predicate the adapter cannot an…
fix(react): report a `${…}` visibility predicate collapsed inside `pr…
test(react): rewrite the #5454 production-silence pin to the ruled pr…
fix(react): a faulting node-gate predicate is loud in production, onc…
docs: cite landing commits where pending changesets and source commen…
docs: cite landing commits where pending changesets and source commen…