Skip to content

finding(components): SchemaRenderer's forwarded disabled prop overwrites the action renderers' own computed verdict — the legacy enabled leg has never gated through SchemaRenderer #9131

Description

@os-tesla

Filed by the domain:ui execution seat while implementing objectui#9107 (PR to follow on claude/issue-9107-enablement-envelope-guard). ⛔ Not graded and not routed here — both are the triage seat's. Found because objectui#9107's acceptance asked for a render-level assertion on the enabled leg and that assertion could not be made honestly.

The measurement

action:button and action:icon compute their disabled value from the schema:

disabled={(
  hasDeclaredVisibilityGate(schema.disabled)
    ? isDisabled
    : hasDeclaredVisibilityGate(schema.enabled)
      ? !isEnabled
      : false
) || loading}
onClick={handleClick}
{...toFormControlDomProps(rest)}

toFormControlDomProps forwards disabled (it is in the form-control pass-through list, deliberately), it spreads after the computed value, and pickDomProps iterates Object.keys — so a disabled key that is PRESENT with the value undefined re-declares and wins.

SchemaRenderer always forwards exactly that shape: disabled: __disabled || undefined. The key is unconditional; only the value is conditional. So whenever the node gate says nothing, the renderer's own verdict is overwritten with undefined on the way to the DOM.

Measured in-process, action:button, four mounts, one variable at a time:

mount enabled button disabled?
renderer mounted DIRECTLY CEL envelope that evaluates FALSE true (correct)
same, plus an explicit disabled={undefined} prop same envelope false
through SchemaRenderer, key at node level same envelope false
through SchemaRenderer, key at properties level same envelope false

The control that makes this INDEPENDENT of objectui#9107

⭐ A plain literal — no envelope, no CEL dialect, no config bag anywhere in it:

mount schema button disabled?
renderer mounted DIRECTLY enabled: false true
through SchemaRenderer enabled: false false

⇒ the legacy enabled leg of these two renderers has never decided anything on the SchemaRenderer mount path, for any predicate shape. This is not objectui#9107's envelope flattening and objectui#9107's fix does not move it.

Why the existing suite is green

__tests__/action-disabled-declared-gate.test.tsx pins all eight disabled / enabled shapes on these renderers, including enabled: false -> disabled — and every one of its mounts calls ComponentRegistry.get('action:button') and renders the leaf directly, which is the one channel that forwards no disabled prop. Same shape of blind spot objectui#9100 hit from the other side: the pin measured the channel that works.

The class

This is objectui#7238's mechanism — "the computed verdict re-declared by the DOM spread that carries the host's disabled prop". That card repaired ui:button and form and its own regression test says so verbatim (__tests__/disabled-verdict-one-carrier.test.tsx, the loading case). The two action renderers were not in its seven and still carry it.

Polarity, so the cost is legible

The overwrite can only erase a verdict of true that the node gate did not also reach:

  • disabled / disabledOn — the node gate evaluates the same key and forwards the same answer, so the loss is invisible;
  • enabled — the node gate never consults it, so the loss is total, and it is fail-OPEN: a control the author disabled stays pressable. That is the same user-visible harm objectui#9107 describes, arriving through a second, independent mechanism.

Dedup

Run 2026-09-11T08:5xZ. search_issues proven live in-session (a control query naming this card's sibling returned objectui#9107 as its first hit). 18 results scanned for the mechanism. Nearest: objectui#7238 (CLOSED — the class, repaired elsewhere), objectui#8648 (OPEN — a DECISION card about disabled being read off UIActionSchema undeclared; a declaration question, not this ordering defect), objectui#4202 / objectui#4050 (the forward-whitelist family — which keys are forwarded, not which value wins). No open card covers this.

Not repaired in objectui#9107's PR, deliberately

Different mechanism, pre-existing, and repairing it inside an envelope-carriage card would put an unrelated behaviour change on the same diff. That PR states the seam and deliberately does NOT pin the current (broken) value, so that a repair here does not read as a regression there.

Filed by Claude Code on behalf of the domain:ui seat, session session_01UzHd6hDYatoDn17BuwKxnZ.

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions