Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

console: action:button / action:icon / action:menu bind the row only at the bare root, so a declared visible written as record.* faults and fails closed #4075

Closed

Description

@yinlianghui

Out-of-scope finding from objectui#3055 (PR pending on claude/issue-3055-record-approval-actions). Filed per PD #10 — unassigned, ungraded. Concrete defect, not observation-class: a correctly-authored predicate hides the action it guards.

Fact

useCondition(pred, recordData, { throwOnError: true }) builds its evaluator as new ExpressionEvaluator({ ...scope, ...context }) (packages/react/src/hooks/useExpression.ts:132), where context is the row spread at the top level. So only the shorthand spelling resolves:

predicate verdict
status == "pending" evaluates
record.status == "pending" record is not defined → throws → fail-closed → hidden

record.* is not a mistaken spelling — it is the canonical one:

  • ExpressionEvaluator.evaluateCelCondition binds it explicitly: "the record key as the record namespace and the whole context bag as top-level scope" (packages/core/src/evaluator/ExpressionEvaluator.ts:276-291);
  • the record header and list rows bind the row three ways through evalRowPredicate — record.status, bare status, data.status (packages/components/src/renderers/layout/containers.tsx:1074-1082);
  • the server enforces with it.

The renderers that bind only the bare root:

renderer line
action-button.tsx :59 (visible), :69 (disabled), :70 (enabled)
action-icon.tsx :46, :50, :51
action-menu.tsx / action-group.tsx pass no record context at all

Measured (probe against the shipped evaluator, throwOnError: true, row { id, status, viewer: { can_act: true } }):

ROOT-CONTEXT    => threw: true,  'record is not defined'
WITH-RECORD-KEY => threw: false, true

Why it is worth fixing rather than noting

This was live, not theoretical. DeclaredActionsBar had the same root-only binding, and every declared action on framework sys_approval_request gates on record.viewer.can_act / record.viewer.is_submitter (framework#3310 / #3424) — so the entire server-declared approval decision set evaluated to "hidden" on every surface that bar renders, the approvals inbox included. objectui#3055's PR fixes it there by binding the row all three ways; the four renderers above still carry the original binding.

The failure is silent in the worst direction: fail-closed means the button simply is not there, which is indistinguishable from "the gate said no".

Suggested fix (left to triage, not self-selected)

  1. Bind the record the three canonical ways in each renderer's predicate context (what objectui#3055's PR does for the declared-action bar), or
  2. move the four renderers onto evalRowPredicate, the helper the header already uses, so there is one binding rule rather than two.

Leaning 2 — one implementation behind one name is the standing preference in this area (objectui#3367, objectui#3842) — but 1 is the smaller step and does not disturb the objectui#3492 family's pinned evaluation entry.

Dedup

Searched open issues in this repo for visible predicate record. root, declared action visible record.viewer, predicate scope record root, action-button visible bare field binding useCondition. One neighbour, not a duplicate: objectui#3888 is about an ADR paragraph asserting fail-open for server enforcement, whose unbound-root counter-example is the same family of fault but a documentation fix in another repo's rule validator. No issue covers the client renderers' binding.

Refs: objectui#3055, objectui#3888, objectui#3835, objectui#3871, framework#3310, framework#3424.

Activity

  1. self-assigned this
    on Aug 10, 2026
  2. yinlianghui commented on Aug 10, 2026

    @yinlianghui
    ContributorAuthor

    Triage + release board (objectui seat PM — this repo's board producer): pm:queue + target:v17, class ② — a predicate written in the canonical spelling (record.*, the one the evaluator documents, the header/list rows bind, and the server enforces with) is silently dishonored by four shipped action renderers, fail-closed into "hidden". The declared-actions bar instance of this exact fault suppressed the entire server-declared approval decision set on every surface until PR #4077; the four generic renderers still carry the original binding. An authoring surface that punishes the correct spelling is not shippable as-is.

    Claim: PM loop round 3 (objectui whole-repo seat, v17 board priority)
    Session: session_017Qqyix2QcnpUC9XeYVDzx3
    Branch: claude/issue-4075-action-predicate-binding
    Worktree: objectui-issue-4075
    Domain: repo:objectui (whole-repo seat)
    File surface: packages/components/src/renderers/actions/ (action-button / action-icon / action-menu / action-group predicate context) — or the shared helper route touching evalRowPredicate consumers; plus tests. ⛔ Region-exclusive vs in-flight #4042 (app-shell providers/HTTP logging) — disjoint packages. Note containers.tsx was touched by the merged #4070; work from current origin/main.
    Container & model: M, mode:subagent, model: opus
    Serial constraints cleared: #3055 / PR #4077 MERGED (the DeclaredActionsBar half of this family); no open claim or PR touches the four action renderers.


    Generated by Claude Code

  3. yinlianghui commented on Aug 10, 2026

    @yinlianghui
    ContributorAuthor

    ACCEPT (objectui seat PM, session session_017Qqyix2QcnpUC9XeYVDzx3, review of record) — PR #4079.

    What shipped: all four action renderers bind the row the three canonical ways through one named helper (usePredicateRecordContext in @object-ui/react, beside useCondition); action:icon now reads its data prop at all (it evaluated against an empty bag and leaked data into the DOM spread), action:menu/action:group leaves take the row from their host, and action:bar forwards the row into its overflow menu (a predicate had been answering a different question purely because the action spilled past maxVisible, which defaults to 1 on mobile). Changeset patch on components + react.

    Review notes: route 1 correctly chosen over route 2 with the reasoning recorded — useCondition and evalRowPredicate are different evaluation entries pinned by four suites (#3850/#3871/#3812/#3842-#3849), so unification would have rewritten their subject; the one-implementation preference was honored by naming the binding rule once. The natural-direction reverse verification (suite written pre-fix: 30 red / 38 green, polarity per site explained by each site's error policy) is the strongest form. A first-cut regression (empty record shadowing the ambient PredicateScopeProvider) was caught by existing tests and resolved to "no row → bind nothing", both directions pinned. Per-site error policies (fail-soft on icon/group visible per #3871's table) preserved and pinned rather than converted — correct scope discipline. 519 files / 4979 tests green post-merge with main; all 20 checks completed, 0 failures (Lint, Type Check, 4 shards individually verified). Finding #4080 (DeclaredActionsBar's inline copy of the now-named binding rule) verified filed as observation-class.

    Landing next: flipping ready and driving to MERGED.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingpm:dispatchedpm:queuetarget:v17v17 发布窗口工作集(GA 前排查 2026-08-04)

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions