Skip to content

fix(components): name a non-array node-level data on data-table instead of dropping it in silence - #6712

Merged
os-sales merged 2 commits into
mainfrom
claude/issue-6665-data-table-string-data-diagnostic
Aug 28, 2026
Merged

fix(components): name a non-array node-level data on data-table instead of dropping it in silence#6712
os-sales merged 2 commits into
mainfrom
claude/issue-6665-data-table-string-data-diagnostic

Conversation

@claude

@claude claude Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Fixes #6665

DataTableRenderer destructures data: rawData = EMPTY_ROWS off the node and then collapses
const data = Array.isArray(rawData) ? rawData : EMPTY_ROWS. So any non-array data an
author wrote becomes zero rows with no error and no warning: the table draws a correct-looking
header over No results found, which reads as a success receipt.

This names it. No behaviour change.

Step 1 — the four-leg table, re-measured

The card quoted this table from skills/objectui/rules/protocol.md, which states it as a
measurement on f1c27f037; nobody had re-run it since, and the triage seat pinned only the
mechanism, not the render. Re-run here on merge-base 5967be095, through the real
SchemaRenderer inside a SchemaRendererProvider holding { customers: [ 2 records ] },
identical columns in every leg, reading tbody tdon a clean tree with none of this
PR's code in it
:

node rendered body cells matches the quoted table
{ "type": "data-table", "data": "${data.customers}", "columns": [...] } No results found (1 tbody tr) yes
{ "type": "data-table", "props": { "data": "${data.customers}" }, ... } No results found (1 tbody tr) yes
{ "type": "data-table", "properties": { "data": "${data.customers}" }, ... } the two rows yes
{ "type": "data-table", "data": [ 2 literal records ], ... } the two rows yes

All four reproduce exactly, and every leg emitted zero [ObjectUI] console lines — the
silence the card is about, measured rather than assumed. The header in the failing leg is
correct (Name, Email), which is why it reads as success.

The contrast IS the argument: the same expression is evaluated under properties and not
at node level. That is what makes this a defect rather than a design choice, so all four legs
are now pinned as one table-driven test instead of living in prose that ages silently.

The predicate

data was authored, and it is not an array.

It deliberately asks a different question than #6575's, rather than widening it. That one
is keyed on an authored bind; these nodes carry no bind at all, so its silence on them
is correct behaviour and not a gap. Widening it would have made that correct silence look like
the defect. Two predicates, one channel — the existing
packages/components/src/renderers/complex/dataTableBindDiagnostic.ts, reused, not duplicated.

It is also deliberately wider than the reported spelling. The ${...} string is what was
reported, but Array.isArray(rawData) ? rawData : EMPTY_ROWS swallows a number, an object, a
null and a plain string exactly as silently, so a predicate keyed on the expression shape
would leave each of them to arrive as a fresh card. The shape only selects a sharper sentence:

  • expression-shaped: data: '${data.customers}'` was never evaluated: ... read as a literal string
  • anything else: ``datawas authored as the number42`, and data-table takes its rows only from an array`

Both end with the one route the guides teach: resolve the rows in the host. The message does
not tell authors to move the expression under properties — whether that is an authoring
channel is an open contract question (#4795), and a console line is the wrong place to settle
it.

What this deliberately does NOT do

Make node-level data evaluate expressions. That is a behaviour change on a published
component; the triage ruling put it on the maintainer floor and dispatched only the diagnostic
arm. Nothing here changes what renders. The trap stops being silent; it does not stop being a
trap.

Nothing is added to the published surface either: the new predicate, message builder and
prefix constant are module-internal and are not re-exported from the package entry — matching
#6575's own symbols.

Tests — red before, green after

Both required cases are pinned, and both were shown red by ablation on the committed tree
(mutation confirmed on disk by grep counts of the removed and injected text, plus a blob-hash
comparison against the HEAD blob; restored via git checkout HEAD -- path, proven by an
empty git diff HEAD). No rebuild step is involved: the root Vitest config aliases every
@object-ui/* specifier to the sibling package's src/, so these suites run against source,
not dist.

ablation expected direction measured
remove the console.warn wiring in data-table.tsx every warning assertion red, every render assertion green 6 failed, 39 passed — the four-leg table and all unit tests stayed green, confirming the render half is behaviour-invariant
narrow the predicate to the ${...} shape only the general non-array assertions red, the expression one green 14 failed, 31 passed — and names the expression string that was swallowed stayed green, so the tests really do forbid a ${...}-only predicate

Green after, on final commit 41dc9a5ff:

pnpm exec vitest run packages/components/src/__tests__/data-table-node-data-diagnostic.test.tsx   packages/components/src/renderers/complex/__tests__/data-table-bind-diagnostic.test.ts --maxWorkers=2
  Test Files  2 passed (2)       Tests  45 passed (45)
pnpm --filter @object-ui/components type-check   -> exit 0
pnpm --filter @object-ui/components lint         -> 930 problems (0 errors, 930 warnings), all pre-existing
check:control-bytes / check:self-import / check:vi-mock-specifiers / check-changeset-no-major -> all OK

tsconfig.test.json includes src/**/*.test.tsx, so the typecheck genuinely covers the new
test file rather than passing by excluding it. Lint was run package-wide rather than
repo-wide; that narrowing is safe here because the diff touches only files inside
packages/components and no config file, and eslint.config.js enables no type-aware linting
(no parserOptions.project / projectService), so no untouched file's verdict can move.

One thing found and filed, not fixed here

Leg 2 of the table — the same expression under props — is a different and wider shape:
properties.* is hoisted onto the node by SchemaRenderer, props is not, so a key under
props never reaches any renderer that reads only schema. This diagnostic is correctly
silent there (node-level data is genuinely absent), and that silence is pinned by its own
test rather than left to be rediscovered. Recorded as #6708 for triage; reaching into props
from a data-table predicate would patch one component against a repo-wide shape.

Changeset

@object-ui/components: patch. Scored to match #6575's own changeset for the same family on
the same file: no behaviour change, no published surface added, no type widened. Not minor
because nothing new is exported from the package entry; never major, per AGENTS.md's version
policy for the fixed release group.


Generated by Claude Code

claude added 2 commits August 28, 2026 17:43
`DataTableRenderer` takes its rows from `data: rawData = EMPTY_ROWS` off the
node and then collapses `Array.isArray(rawData) ? rawData : EMPTY_ROWS`, so
ANY non-array `data` an author wrote becomes zero rows with no error and no
warning: the table draws a correct-looking header over `No results found`,
which reads as a success receipt.

The reported spelling is a `${...}` expression string — the same expression is
evaluated under `properties` and NOT at node level — but the swallow is
general, so the predicate is too: `data` authored and not an array. The
expression shape only selects a sharper sentence.

Reuses the objectui#6575 channel (`dataTableBindDiagnostic.ts`) as a second
predicate rather than a widened one: these nodes carry no `bind` at all, so
that diagnostic's silence on them is correct behaviour, not a gap.

No behaviour change. The trap stops being silent; it does not stop being a
trap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
Scored patch, matching objectui#6575's own changeset for the same family on the
same file: no behaviour change, and nothing added to the published surface —
the new predicate, message builder and prefix constant are module-internal and
are not re-exported from the package entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 49 chunks) 3232.4 KB 3266.6 KB
Main entry chunk (gzip) 157.2 KB 350 KB
Entry file index-CyGlFgJT.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 11.89KB 4.50KB
app-shell (runtime-config.js) 20.61KB 7.35KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 510.58KB 116.01KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 173.10KB 47.96KB
fields (index.js) 239.05KB 60.06KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.53KB 3.38KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.64KB 1.50KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 1.93KB 0.88KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.89KB 12.91KB
plugin-charts (index.js) 64.66KB 18.32KB
plugin-chatbot (index.js) 190.33KB 45.10KB
plugin-dashboard (index.js) 133.26KB 34.43KB
plugin-designer (index.js) 212.80KB 43.15KB
plugin-detail (index.js) 245.29KB 62.39KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 132.01KB 32.23KB
plugin-gantt (index.js) 165.20KB 40.37KB
plugin-grid (index.js) 201.51KB 54.54KB
plugin-kanban (index.js) 53.11KB 14.62KB
plugin-list (index.js) 113.01KB 27.57KB
plugin-map (index.js) 20.17KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 26.44KB 7.59KB
plugin-tree (index.js) 9.00KB 3.08KB
plugin-view (index.js) 85.87KB 21.12KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 67.73KB 22.54KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 2.44KB 1.21KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sales
os-sales added this pull request to the merge queue Aug 28, 2026
Merged via the queue into main with commit 4c68077 Aug 28, 2026
30 checks passed
@os-sales
os-sales deleted the claude/issue-6665-data-table-string-data-diagnostic branch August 28, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

data-table: a ${...} expression authored in node-level data is not evaluated and renders an empty body silently

2 participants