Skip to content

Three gaps in the visibleWhen gate found by census: one authored-node bypass, a second evaluator with an opposite data binding, and total silence on fault in production #5926

Description

@claude

Rebuilt from objectstack-ai/objectstack#11258 by the triage seat (session session_01Kktexqp6uVuFMztvvTMf3V, 2026-08-24 concentration round) — file-at-destination: all three gaps land in this repo; the transfer API is unavailable to this session, so the card is rebuilt verbatim and the source closed as moved. Bare issue refs in the original text refer to objectstack-ai/objectstack unless spelled otherwise.


Filed unassigned as a finding — ⛔ recording only, not graded, no domain:*, no repo:* set.

⚠️ All three land in objectstack-ai/objectui, not here. Filed in this repo because the measuring dev was fenced out of objectui by its dispatch and routed them to the PM rather than minting cross-repo cards unasked — the same call it made in round 1, and the right one. Handing to triage, which owns routing and transfer. Same disposition as #11247.

Found by the block-registry census that PR #11254 (#9167 round 2) ran to close round 1's recorded confidence gap. All three sit beside a strong positive result, which is the context they should be read in:

visibleWhen is not a per-block concern at all. It is enforced once, generically, in packages/react/src/SchemaRenderer.tsxshouldHide tests visibleWhen first (ahead of the hoisted visible, since objectui#5454), sets _hidden, and if (evaluatedSchema._hidden) return null fires before the registry dispatches. A block renderer cannot ignore the gate because it never sees the node. Verified behaviourally on three block types this card does not touch (record:path, record:highlights on both a showcase and a platform page), each with a paired must-show mount.

So the gate is structurally sound. These are the exceptions to it.


1. emptyAction bypasses the central gate — the only authored-node bypass in the tree

renderers/complex/data-table.tsx:1974 renders the emptyAction slot by resolving the registry directlyconst Comp = ComponentRegistry.get(node.type); return <Comp schema={node} /> — so it never passes through SchemaRenderer and a visibleWhen on that node is never evaluated.

This is the actionable one. It is the same declared-not-enforced class objectui#5401/#5505 just closed for record:alert, one level down: an author writes a gate, the platform accepts it, and nothing enforces it.

⚠️ Distinguish it from the other direct-registry render, action-bar.tsx:300 — that is the ADR-0089 action face, which gates on visible inside action-button/action-icon by design. Not a bypass; do not "fix" it.

2. page:tabs runs a second evaluator whose data means the opposite thing

containers.tsx:449 builds its own ExpressionEvaluator for item-level visibleWhen, and its root differs from the node-level gate's in a way that is worse than a mere difference:

  • item-level: spreads the row flat and binds data to the row
  • node-level: binds data to the data-source adapter

Same key, opposite meanings. A predicate correct in one position is silently wrong in the other. It also calls evaluateCondition without the dev throwOnError probe, so a faulting tab predicate is silent where a faulting node predicate warns.

Nobody is measurably hurt today — recorded at that severity, not inflated.

3. In a production bundle, a node-gate fault is entirely silent

evaluateVisibilityPredicate short-circuits on if (!__DEV__).

This is the one I would not have wanted dropped, because it changes how the other two should be read. The bare-string breakage measured during #11254 produced no console line at all — the gate stopped gating, on a real page, in total silence. (Round 1's "has" is not a function lines came from a different face: record-alert.tsx's useCondition path, which logs regardless.)

So a production deployment gets no signal when an authored gate silently stops working. That is the mechanism by which a class-1 defect ships unnoticed.

⚠️ It is also a deliberate performance short-circuit, so the fix is a design question — what diagnostic budget production should carry — not a bug report. Grading it as a bug would misread it.


Suggested disposition

The implementing dev recommended filing only (1) and holding (2) and (3) as observations. I am filing all three in one card rather than one card or three, on two grounds: they are a single census and splitting is triage's call, not mine; and (3) is the reason (1) is dangerous, so separating them discards the connection that makes either worth acting on.

⛔ Nothing was migrated or changed on the strength of this census — PR #11254 touched exactly the two predicates its card ruled, and a wider migration is a separate card.


Generated by Claude Code

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions