Skip to content

finding(components): seven widgets re-read the raw schema.disabled beside SchemaRenderer's evaluated verdict — 9 TS2322 once the #7087 narrowings drop, and ui:button / form grey out on any predicate string #7238

Description

@os-litant

Filed unassigned while building objectui#7087 (branch claude/issue-7087-disabled-narrowing, maintainer ruling 2026-09-01 option 1: the 18 concrete schemas' disabled?: boolean narrowings drop and disabled inherits BaseSchema's boolean | string). The dispatch's Zone-2 item 3 asked for the readers to be verified rather than assumed, and to STOP rather than widen any reader that consumes disabled as a strict boolean. This is that report. The #7087 PR touches packages/types only; nothing here is fixed there.

What holds

The ruling's premise is true at the renderer. packages/react/src/SchemaRenderer.tsx (lines 1289-1299 on origin/main @ 67dadd602) gates disabled / disabledOn through hasDeclaredPredicate + evaluateEnablementPredicate, sets _disabled, strips the raw key from the outgoing spread (line 1463) and forwards the VERDICT as a real disabled prop (disabled: __disabled || undefined, line 1584). ActionSchema.disabled's three readers are predicate-aware already: packages/core/src/actions/ActionRunner.ts:1004-1015, packages/app-shell/src/views/DeclaredActionsBar.tsx:179 / :394, packages/components/src/renderers/complex/data-table.tsx:401.

What does not

The schema the renderer hands the component is evaluatedSchema itself (schemaForComponent, lines 1343-1366) — the RAW authored disabled stays on it. Seven in-scope widgets read that raw value beside the host verdict. Measured with the #7087 change applied and the closure built, pnpm --filter @object-ui/components type-check on 06103d1 reports 9 × TS2322 Type 'string | boolean | undefined' is not assignable to type 'boolean | undefined':

(a) DOM passthrough, compile-only — disabled={schema.disabled}:

  • packages/components/src/renderers/form/input.tsx:48
  • packages/components/src/renderers/form/textarea.tsx:45
  • packages/components/src/renderers/form/checkbox.tsx:44
  • packages/components/src/renderers/form/select.tsx:52
  • packages/components/src/renderers/form/combobox.tsx:28
  • packages/components/src/renderers/disclosure/collapsible.tsx:20

Runtime is correct through SchemaRenderer for these six: the host's forwarded disabled prop is spread AFTER the raw one (toFormControlDomProps keeps disabled by name — packages/components/src/lib/form-control-dom-props.ts; pickDomProps iterates Object.keys, so an undefined verdict overrides too; collapsible spreads props bare). The narrowing was load-bearing for their COMPILE, not their behaviour.

(b) raw read that drops the verdict, runtime defect — packages/components/src/renderers/form/form.tsx:1008 destructures disabled from schema (FormSchema) and strips the host prop unused (disabled: _disabledProp, line 2804). The raw value feeds :2653 (every field's disabled), :2994 (cancel button), :3006 (submit button) — the three form.tsx errors. A predicate string is truthy however it evaluates: disabled: "${data.status === 'locked'}" on a form greys the whole form out even when the predicate is false.

(c) truthy OR, runtime defect, compiles — packages/components/src/renderers/form/button.tsx:42: const isDisabled = schema.disabled || props.disabled || isLoading; — props is any, so no TS error, but a predicate string on ButtonSchema.disabled disables the button regardless of verdict.

Not affected (no raw read; they take the host prop through their spread): radio-group, switch, toggle, slider, date-picker, calendar, input-otp, file-upload, toggle-group (group-level).

(b) and (c) are reachable TODAY through JSON metadata and through any BaseSchema-typed node, independently of #7087 — the base type has advertised the predicate since #4581. #7087 makes them reachable from the concrete TS types too, and turns (a) from silent into a type error.

The fix shape already in the tree

packages/plugin-chatbot/src/renderer.tsx (objectui#6169) consumes the host-evaluated prop instead of re-reading the raw key — its comment: the raw value may be an expression STRING, which is truthy however it evaluates — one carrier for one question (AGENTS.md #0.1). The same move at the nine sites (read the forwarded disabled prop; in form.tsx stop discarding _disabledProp) removes the 9 TS2322 and the two runtime defects without a cast. Widening a reader is Clause-② content beyond the #7087 ruling, so it is filed here for routing rather than folded into that PR: either it rides in the #7087 PR under the same contract review, or it lands first and the #7087 PR turns green on rebase-by-queue. Until one of those, the #7087 PR's Type Check job is expected red on exactly these nine lines.

Refs #7087 · #6169 · #4581.

Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    on Sep 2, 2026
  2. self-assigned this
    on Sep 2, 2026
  3. os-litant commented on Sep 2, 2026

    @os-litant
    CollaboratorAuthor

    Claim: PM loop round 1
    Session: session_01NRRumy89BYdW9ogbcdHTho
    Branch: claude/issue-7238-disabled-one-carrier
    Worktree: objectui-issue-7238
    Domain: domain:ui
    File surface: packages/components/src/renderers/form/input.tsx, textarea.tsx, checkbox.tsx, select.tsx, combobox.tsx, form.tsx, button.tsx, packages/components/src/renderers/disclosure/collapsible.tsx, tests under packages/components/src/, one changeset (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus — node scripts/pm/dispatch-gates.mjs --tier over these paths is objectstack-derived and a floor only; the content limb does not fire: the widgets converge on the verdict the renderer already forwards, no type moves, no accept set changes (the accept-set change is #7087's, reviewed separately on PR #7240)
    Clause-②: no
    Serial constraints cleared: PR #7240 (#7087) is parked draft and touches only packages/types — this card lands FIRST and unblocks it; PR #7241 (#7188, landing) touched packages/components/src/renderers/complex/data-table.tsx, disjoint from these files; #7216 is in flight on plugin-list. No other card in the lane names these eight files. Same-day churn is checked by the dev's own fetch at dispatch.

    Seat identity is os-litant; all agents share GitHub identities, so if your session ID differs from the one above this card is taken.

    Grading note (代扫): the objectui triage seat is not in session; graded by the execution seat as bug · priority:p2 · domain:ui (published library; the runtime defect in form / ui:button is reachable through JSON today) and dispatched in the same pass because it is the dependency of a parked, maintainer-ruled PR.

    No ruling on this card. The binding context is the #7087 ruling (option 1 — disabled is boolean | string on every BaseSchema extender) and the one-carrier rule: the renderer evaluates the predicate and forwards the verdict as the disabled prop; widgets read that carrier, never the raw key.

    Readings (the card's own, measured 2026-09-02 01:30Z on 67dadd602 with #7087's diff applied): six DOM passthroughs disabled={schema.disabled} (input.tsx:48, textarea.tsx:45, checkbox.tsx:44, select.tsx:52, combobox.tsx:28, collapsible.tsx:20) — compile-only once the narrowing drops; form.tsx:1008 destructures the raw disabled and discards _disabledProp (:2804), feeding :2653 / :2994 / :3006; button.tsx:42 ORs the raw value. Premise stands.


    Generated by Claude Code

  4. os-litant commented on Sep 2, 2026

    @os-litant
    CollaboratorAuthor

    Claiming this card.

    • session: session_01NRRumy89BYdW9ogbcdHTho (dev, dispatched by the domain:ui seat)
    • branch: claude/issue-7238-disabled-one-carrier (pushed empty as the write-route probe)
    • worktree: /home/user/objectui-issue-7238, base 67dadd602

    Scope: the eight files named in the card (six DOM passthroughs, form.tsx, button.tsx) plus tests and one changeset for @object-ui/components. packages/types is not touched here (that is #7087 / PR #7240). renderers/complex/data-table.tsx is not touched (PR #7241).

    Generated by Claude Code


    Generated by Claude Code

  5. os-litant commented on Sep 2, 2026

    @os-litant
    CollaboratorAuthor

    os-dev-report

    {
      "issue": 7238,
      "status": "done",
      "branch": "claude/issue-7238-disabled-one-carrier",
      "pr": "https://github.com/objectstack-ai/objectui/pull/7318",
      "premise_still_valid": true,
      "summary": "All nine sites converged onto SchemaRenderer's evaluated verdict, in the plugin-chatbot (#6169) shape: each of the eight files now destructures the forwarded `disabled` prop by name and no widget reads `schema.disabled`. form.tsx stops discarding the host prop (`_disabledProp` removed, `const disabled = hostDisabled === true`); button.tsx computes `isDisabled = hostDisabled || isLoading`. packages/types untouched. ONE CARD SUB-CLAIM FALSIFIED, and the PR body says so rather than shipping the prediction: claim (c), that a predicate string greys ui:button out, is NOT what the tree does. `disabled` also rode buttonProps into toFormControlDomProps, which forwards it by name and keeps the key even when the value is `undefined` (pickDomProps iterates Object.keys); spread AFTER `disabled={isDisabled}`, the host verdict already won, so both predicate polarities were correct. The real ui:button runtime defect is the `loading` leg of that OR, which never reached the element: `loading: true` rendered the spinner on a live, clickable button. Card claims (a) and (b) hold exactly as written. HEAD 78be0e9b2 = my commit f1f3fa6ef plus two merges of origin/main (64d624ded, then d717e8bc3); PR shows mergeable_state `behind` because main moved again during the run. Files changed 11, +304/-24.",
      "files_changed": [
        "packages/components/src/renderers/form/input.tsx",
        "packages/components/src/renderers/form/textarea.tsx",
        "packages/components/src/renderers/form/checkbox.tsx",
        "packages/components/src/renderers/form/select.tsx",
        "packages/components/src/renderers/form/combobox.tsx",
        "packages/components/src/renderers/form/form.tsx",
        "packages/components/src/renderers/form/button.tsx",
        "packages/components/src/renderers/disclosure/collapsible.tsx",
        "packages/components/src/__tests__/disabled-verdict-one-carrier.test.tsx (new, 206 lines)",
        "packages/components/src/__tests__/form-renderers.test.tsx (the one direct-usage census hit)",
        ".changeset/7238-disabled-one-carrier.md (patch, @object-ui/components)"
      ],
      "direct_usage_census": "None of the eight files exports its component, so the only host-free mount is ComponentRegistry.get(...). Enumerated every such call site across packages/, apps/, examples/. NO production path renders any of the eight directly: plugin-form composes { type: 'form' } nodes and hands them to SchemaRenderer with no root-level `disabled`; every other consumer goes through the renderer. So no boolean-only raw read was kept anywhere. Exactly ONE test relied on the raw read - form-renderers.test.tsx 'should handle disabled state', which mounted renderComponent({ type: 'input', disabled: true }) with no host; it now renders through the real SchemaRenderer. Every other direct-render test authors `disabled` on a FIELD (fields: [{ ..., disabled: true }]), which travels the untouched fieldDisabled path at form.tsx:2135. Also noted: packages/components/src/SchemaRenderer.tsx is a dead unexported second renderer that spreads the raw schema and evaluates nothing - filed as #7319, not touched.",
      "ts2322_probe": "06103d1d6 (claude/issue-7087-disabled-narrowing) merged in as a scratch merge, @object-ui/types REBUILT first (packages/components overrides the root tsconfig `paths`, so it resolves @object-ui/types through node_modules to dist/index.d.ts - an unbuilt probe would have measured the old narrowings), then `pnpm --filter @object-ui/components run type-check`. WITH the fix: 0 error TS2322, 0 total TS errors. With the eight files reverted to 67dadd602: exactly 9 error TS2322, exit 2 - form.tsx x3, input/textarea/checkbox/select/combobox/collapsible x1 each; button.tsx contributes 0 because `props` is `any` there. 9 -> 0. Scratch merge aborted before committing and the restore verified: empty `git diff HEAD`, no MERGE_HEAD. packages/types is NOT in this PR.",
      "gates": {
        "vitest packages/components/ packages/fields/ packages/react/ @ f1f3fa6ef": "Test Files 422 passed (422) / Tests 5041 passed (5041)",
        "vitest packages/components/ @ 32b214a04": "Test Files 224 passed (224) / Tests 2065 passed (2065)",
        "turbo run build --filter='@object-ui/components^...' @ 78be0e9b2": "Tasks: 7 successful, 7 total",
        "turbo run type-check --filter=@object-ui/components @ 78be0e9b2": "Tasks: 9 successful, 9 total; 0 error TS (runs `tsc --noEmit && tsc -p tsconfig.test.json`, so the new test file IS type-checked - not the NOT-MEASURED trap)",
        "pnpm --filter @object-ui/components lint (eslint .) @ f1f3fa6ef": "944 problems (0 errors, 944 warnings) - all pre-existing; this is the package's own CI unit, not a narrowing",
        "check:control-bytes @ 78be0e9b2": "OK (scanned 6014 tracked text file(s); skipped 85 binary)",
        "check:vi-mock-specifiers @ 78be0e9b2": "OK (4135 tracked source file(s), 2404 test-named; 0 non-static)",
        "check:vi-mock-inherit @ 78be0e9b2": "OK (118 call site(s) on @object-ui/react judged: 118 inherit, 0 auto-mocked)",
        "check:self-import @ 78be0e9b2": "No package names itself inside its own src/",
        "check:phantom-deps @ 78be0e9b2": "Every in-scope import is declared by the package that publishes it",
        "check-changeset-presence.mjs @ 78be0e9b2": "10 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s): .changeset/7238-disabled-one-carrier.md",
        "check-changeset-no-major.mjs @ 78be0e9b2": "No changeset declares a `major` bump",
        "check:sdui-registration-pins @ f1f3fa6ef (console built first)": "All 16 registration(s) a sideEffects array promises are present in the built console (517 chunks weighed; the 3 ruled control(s) are in the derived set)",
        "check:eager-closure @ f1f3fa6ef (console built first)": "exit 0"
      },
      "tests": "PRE-FIX MEASUREMENT on the unmodified tree @ 67dadd602 - `pnpm exec vitest run packages/components/src/__tests__/disabled-verdict-one-carrier.test.tsx` reported `Tests 2 failed | 23 passed (25)`: form FALSE-predicate red (AssertionError: expected true to be false) and ui:button `loading` red (expected false to be true). The 23 green are the discriminating controls - the TRUE-predicate polarity of every case, the literal `disabled: true` polarity, and BOTH polarities of all six DOM pass-throughs; a one-polarity pin would have gone green on a widget that dropped `disabled` entirely. Every case renders through the real SchemaRenderer and the real registry, with the predicate resolved against SchemaRendererContext's dataSource. POST-FIX the same file is 25 passed (25). ABLATION, taken AFTER the commit so the restore leg has a real reference: form.tsx and button.tsx reverted to 67dadd602. Mutation confirmed ON DISK by anchor text in both directions, not by an exit code - `hostDisabled` count went to 0 in both files, `disabled = false,` back to 1 in form.tsx, `schema.disabled || props.disabled` back to 1 in button.tsx, and `git diff HEAD --stat` non-empty (2 files, 6 insertions, 37 deletions). Mutated run: `Tests 2 failed | 23 passed (25)` - the same two cases, the 23 controls still green. NO REBUILD LEG IS OWED and this is the stated condition, not an omission: the root vitest.config.mts aliases @object-ui/components (line 284) and @object-ui/react (line 282) to packages/*/src, so the resolution path never touches dist/ and the mutated SOURCE is what runs. Restore leg given the same treatment as the mutation leg: `git checkout HEAD -- ABSOLUTE_PATH` (HEAD named explicitly, never a bare checkout, so a poisoned index cannot serve it), proven by an EMPTY `git diff HEAD` AND a `git hash-object` match against each file's HEAD blob (af6f4bc30954e0a4baa4d6bd4509560a9e3d1802 for form.tsx, f5f1a0a0d597485019771ae58569ba42ab2a0fc7 for button.tsx; an empty hash would have been read as failure), then `Tests 25 passed (25)`. The whole script carried a `trap restore EXIT INT TERM` with absolute paths resolved from `git rev-parse --show-toplevel`.",
      "deviations": [
        "DECLARED NARROWING - the suite re-run on the FINAL head 78be0e9b2 did not land. The shared verify lock was held continuously by the sibling agent in /home/user/objectui-issue-6190 (holder pid 2444, a single repo-wide chain of build + type-check + vitest + lint, still held at 1563s); three bounded acquisitions with unlocked work in between (~24 minutes of queueing) all returned exit 99 without acquiring. The narrowing is a measurement, not a skip: `git diff --stat 32b214a04 78be0e9b2 -- packages/components/src` is ONE added file, page-header-action-ids.dist.spec.tsx, which matches DIST_PIN_GLOB (packages/*/src/**/*.dist.spec.tsx, vitest.config.mts:167) and therefore runs only in the opt-in `dist` project behind OBJECTUI_DIST_PINS=1 (line 186) - never in the unit/dom/dom-heavy projects a `packages/components/` path filter drives. No production source under packages/components/src moved between the last full green and HEAD. The merge's other content (packages/types, packages/plugin-form) is covered against this package by the green type-check ON 78be0e9b2. CI runs the full farm on the PR regardless.",
        "check:sdui-registration-pins and check:eager-closure are quoted at f1f3fa6ef (which contains 100% of my diff) rather than at 78be0e9b2, for the same lock reason - both need a fresh console bundle, a 4m20s build. First attempt at them returned PREREQUISITE NOT MET (exit 2: 'No console build to weigh at apps/console/dist/assets. This is exit 2, not a pass'); the quoted greens are from after the console was built, not from that run.",
        "form-renderers.test.tsx was edited - it is inside the declared file surface (tests under packages/components/src) and it is the single direct-usage census hit, so leaving it would have been the raw read surviving in a pin."
      ],
      "mcp_calls": "6 - add_issue_comment (claim), create_pull_request, pull_request_read (PR body readback), search_issues (dedupe for the finding, with #7238 itself returned as the known-must-hit control), issue_write (filed #7319), add_issue_comment (this report). Card body and comments were read through the public-repo embedded JSON payload at zero quota.",
      "open_questions": [],
      "out_of_scope_findings": [
        "filed as #7319: packages/components/src/SchemaRenderer.tsx is a dead second renderer with the same export name as the real one - nothing imports it, it is not in the package barrel, and it evaluates no gate at all (bare truthy `hidden` check, and `{...schema}` spreads the RAW node, so it would defeat exactly the one-carrier contract this card establishes). Labelled `finding`, unassigned, recommendation: delete."
      ]
    }

    Generated by Claude Code


    Generated by Claude Code

  6. os-litant commented on Sep 2, 2026

    @os-litant
    CollaboratorAuthor

    ACCEPT — PR #7318 reviewed by the domain:ui seat (session session_01NRRumy89BYdW9ogbcdHTho), landing gated on CI.

    Read by content, not by the report. Branch fetched locally; three-dot diff against origin/main is exactly the 11 files the report lists (+304/−24). The eight widgets destructure the forwarded disabled prop by name; form.tsx drops disabled = false off schema and the _disabledProp discard, and reads hostDisabled === true; button.tsx takes disabled out of props so toFormControlDomProps can no longer overwrite isDisabled. Residual raw reads at the PR head: a grep for schema.disabled across the eight files hits only three comment lines; the same grep at the fork point 67dadd602 hits input.tsx ×1 and button.tsx ×1 (the control that proves the grep is live). hostDisabled appears 3× in each of the five untouched-by-me pass-throughs.

    Report corrections accepted. The card's sub-claim (c) — a predicate string greys ui:button out — was falsified by measurement: the host verdict already overwrote the raw read through the DOM pass-through spread, so the real casualty was the loading leg. The PR body reports the measurement instead of the prediction; the card's (a) and (b) stand. Recorded here so the card's text is not read as the tree's behaviour.

    Gates. check-governed-merges.mjs --test on the final file list → NOT governed (ordinary queue landing). Commit trailers present on f1f3fa6ef; no model identifiers in the diff. Changeset present, patch on @object-ui/components, no major. At review time (06:34Z) the PR's 30 check runs read: Lint, Bundle Analysis, README Export Check, Doc Snippet Type Check, Build & E2E, Governed Surface Queue Guard and every changeset gate completed: success; Type Check and Test (shard 1–4) still in_progress. The dev's declared narrowing (no full-suite re-run on the final head because the shared verify lock was held by the sibling #6190 chain; the only source delta between the last green and the head is a *.dist.spec.tsx file outside the unit projects) is accepted as a measurement, and CI's full farm is the closing read.

    Not reproduced locally: the dev's worktree was removed at hand-off and the shared checkout carries no node_modules, so the 2-red/23-green pre-fix reading and the ablation are taken from the report's on-disk-verified evidence plus CI, not from a seat-side run.

    Landing plan. Once Type Check and the four test shards are completed: success: ready + auto-merge (SQUASH), enqueue confirmed positively via git ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-7318-*'. After it lands: execute Unlock-action: re-check PR #7240 on #7087 (merge origin/main into claude/issue-7087-disabled-narrowing, confirm Type Check green on the new head, then ready + auto-merge). Side finding #7319 (dead second SchemaRenderer in components) stays finding, unassigned.


    Generated by Claude Code

  7. os-litant commented on Sep 2, 2026

    @os-litant
    CollaboratorAuthor

    Landed — PR #7318 merged as b9f5ff128 (squash, one parent ad3d4029a; merged 2026-09-02T07:00:11Z).

    Landing confirmed by content on origin/main, not by PR head sha:

    • git grep -c 'hostDisabled' origin/main -- packages/components/src/renderers/form/form.tsx → 2; the same probe at the fork point 67dadd602 → 0 (control).
    • schema.disabled || props.disabled in button.tsx survives only inside the explanatory comment on line 49 — the live OR is gone.
    • packages/components/src/__tests__/disabled-verdict-one-carrier.test.tsx and .changeset/7238-disabled-one-carrier.md present on origin/main.
    • Merge-queue refs for PR 7318: 0 remaining (drained; PR 7320's entry now sits on this build).

    Card closed by the PR's Fixes line; pm:dispatched stripped in the same step (labels now bug, priority:p2, domain:ui). Next: the Unlock-action: re-check PR #7240 on #7087 runs now — origin/main merges cleanly into claude/issue-7087-disabled-narrowing (read-only git merge-tree reports no conflicts), so the branch is updated from main and its Type Check is re-read before it goes ready. Session session_01NRRumy89BYdW9ogbcdHTho.


    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 workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions