Skip to content

finding(types,components): DataTableSchema.emptyAction declares SchemaNode on both faces while the renderer admits only objects — a bare string is silently dropped, the identical defect objectui#7105 just closed one slot over #8331

Description

@os-zhuang

Observation-class finding, measured while accepting PR #8330 (objectui#7105). Filed unassigned, no labels — grading is the triage seat's.

This one arrives with its ruling already made, one slot over. objectui#7105 (director seat, decision batch #69, 2026-09-07, maintainer 「其他同意」) ruled exactly this shape on EmptySchema.action: the declaration says SchemaNode, the renderer required an object, and the ruling's answer was relax the renderer, not narrow the declaration — "a primitive node is handed to SchemaRenderer like any other node slot (a string renders as text), so a bare string is no longer silently dropped."

The claim

DataTableSchema.emptyAction is declared SchemaNode on both faces and rendered only when it is an object. A bare string node — legal by the declaration, and what SchemaRenderer renders as text — is silently dropped.

Measured on origin/main = fedfa3e

packages/types/src/data-display.ts:1148        emptyAction?: SchemaNode;
packages/types/src/zod/data-display.zod.ts:285 emptyAction: SchemaNodeSchema.optional().describe('Optional schema node rendered inside the empty-state, …')
packages/components/src/renderers/complex/data-table.tsx:2160
    {schema.emptyAction && typeof schema.emptyAction === 'object' && (
:2161  <SchemaRenderer schema={schema.emptyAction} />

⇒ Both declared faces admit any SchemaNode; the render path admits only the object subset. Declared wider than enforced, in the direction that loses the author's content without a diagnostic.

⚠️ The path is renderers/complex/, not renderers/data/ — recorded because I searched the wrong directory first and got a clean zero. A zero from the wrong path is not a finding, and this is the sort of near-miss that turns a real defect into a "does not reproduce".

Why this is the same defect, not merely a similar one

The behaviour SchemaRenderer gives a primitive is already pinned in this repo — packages/react/src/__tests__/SchemaRenderer.primitiveSchema.test.tsx (objectui#4548): a non-empty string renders as its own text, a number as '42', a boolean as 'true', and '' / 0 / false render nothing. ⇒ The object-only guard is not protecting the renderer from anything it cannot handle; it is discarding input the declaration promises to accept.

⚠️ One thing that is NOT settled by #7105's ruling and needs measuring here, because this slot has history the other did not: data-table.tsx:2131-2144 carries a docblock about this very slot, and packages/components/src/renderers/complex/__tests__/data-table-empty-action-visible-when.test.tsx pins visibleWhen behaviour on it — including the note that the empty state is the only place it renders. ⇒ Whoever takes this reads both before touching the guard; relaxing it must not disturb the visibleWhen gate that test pins.

The remedy #7105 shipped, for whoever takes this

Not a template to copy blind, but the shape is settled: declaration unchanged, guard relaxed, the value handed to SchemaRenderer through the repo's existing bridge (toRenderableSchema in packages/react/src/schema-input.ts, a documented-PERMANENT total function that maps number and boolean onto the text form the renderer's own defensive branch produces — SchemaRendererProps.schema deliberately excludes those two per #4548 ruling Q2). ⛔ No coercion needs inventing.

⚠️ It is still a behaviour change on a published surface and owes its own ruling, exactly as #7105 did — a node that renders nothing today starts rendering. ⛔ Do not treat #7105's adoption as pre-authorising this one; what it settles is the shape of the answer and the fact that the bridge exists.

Why it was not fixed in place

The implementing session found it and correctly left it: data-table.tsx is outside objectui#7105's declared file surface, it is a large hot file whose claim state it could not cheaply establish, and the honest remedy is a behaviour change. ⇒ Reported to the seat rather than absorbed — the right call, and the reason this card exists instead of a widened PR.

Refs

Filed by the domain:spec @ objectui PM seat, session session_01QtGhnU3WnnWyiWeYQhw2aX, 2026-09-07T12:56Z.


Generated with Claude Code

https://claude.ai/code/session_01QtGhnU3WnnWyiWeYQhw2aX

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 seatfindingpackage: componentspriority:p2

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions