Skip to content

fix(plugin-grid): give a dropped ObjectGrid column an address instead of silence - #5456

Merged
os-support-ai merged 2 commits into
mainfrom
claude/issue-5349-objectgrid-silent-no-columns
Aug 21, 2026
Merged

fix(plugin-grid): give a dropped ObjectGrid column an address instead of silence#5456
os-support-ai merged 2 commits into
mainfrom
claude/issue-5349-objectgrid-silent-no-columns

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Fixes#5349

#5068 retired ObjectGrid's undeclared accessorKey / header tolerance branch, so
ListColumnSchema's field / label is now the only column spelling the renderer
reads. That was right. What it left behind was the receipt: a column authored in a
spelling the renderer does not read contributed nothing, and nothing said so — no
error, no warning, no empty state. The author got a grid with its row-number column and
no data columns, i.e. a success receipt for a disagreement between renderer and author.
The same shape #5068 exists to fix, one level down.

An authored column that can never resolve now emits oneconsole.warn naming the
address rather than the symptom.

The diagnostic

Verbatim, from the accessorKey-only leg of the test suite:

[ObjectUI] ObjectGrid columns: object-grid (objectName: 'opportunities') — 2 of 2 authored columns resolve to nothing, so this grid renders its row-number column and NO data columns.
• columns[0]: keys seen: `accessorKey`, `header` — `accessorKey` / `header` are refused by name and not read here. Write `{ field: 'name', label: 'Name' }` instead.
• columns[1]: keys seen: `accessorKey`, `header` — `accessorKey` / `header` are refused by name and not read here. Write `{ field: 'amount', label: 'Amount' }` instead.
`ListColumnSchema` (@objectstack/spec/ui) declares ONE column spelling: `field` (required — the field name) and `label` (the header). A plain `string[]` of field names works too. (objectui#5349)

Which block (the view:grid alias is named as view:grid), which object and label,
which columns[i], the keys that entry actually carries, and the rewrite that works —
built from what was authored, not from a template.

Channel.ObjectGrid already had exactly one channel for "you declared it, the
renderer dropped it": the export-format warning ([ObjectUI] ObjectGrid export: unsupported format(s) hidden from the menu…) — a useEffect keyed on the schema slice,
one console.warn, no NODE_ENV branch. This is deliberately the same shape rather
than a second, differently-shaped one next to it, which the card explicitly warned
against. A visible in-grid message would be user-facing copy needing all ten locale packs
in @object-ui/i18n — outside this card's file surface — and a throw was rejected on the
card's own reasoning: a grid that renders nothing today would become a page that renders
nothing.

Measured matrix, before and after

Rendering is unchanged in every leg — the diagnostic is additive. Readings are the
literal getAllByRole('columnheader') / getAllByRole('cell') output over two inline
rows (# is the built-in row-number column, Open the row affordance):

inputheaders, beforeheaders, afterdiagnostics, beforediagnostics, after
[{field:'name'},{field:'amount'}]["#","Name","Amount"]same00
[{accessorKey:'name'},{accessorKey:'amount'}]["#"], no data cellssame01 — names columns[0] and columns[1]
[{field:'name'},{accessorKey:'amount'}]["#","Name"]same01 — names columns[1] only
[{accessorKey:'amount'},{field:'name'}]["#","Name"]same01 — names columns[0] only
['name','amount']["#","Name","Amount"]same00
[{field:'name',hidden:true}]["#"]same00 — authored intent
no columns key["#","Id","Name","Amount"]same00
columns: []["#","Id","Name","Amount"]same00

The hidden row is the one that decides the design. It renders identically to the
accessorKey-only row — header-only — and it is correct. So the predicate cannot be
"the grid ended up with no columns".

What the predicate asks, and what it refuses to ask

It reads the columnsinput and nothing else: the author wrote this entry and it
can never resolve — say which one
. It never asks whether the grid found rows,
because object-grid legitimately draws them from five different places, and a
"needs columns" predicate consulting any of them would eventually paint a configuration
error over a working grid.

All five are pinned by test, in both directions — clean columns must render and stay
silent, then the same configuration with one mis-spelled column must produce exactly one
diagnostic (that second leg is the counter-probe that makes the first leg's zero mean
something):

fallback honouredpinned by
inline rows as a bare data arraycolumnSpellingDiagnosticRender.test.tsx
data: { provider: 'value', items }same
legacy staticDatasame
bind — rows from the surrounding data scopesame
a host that owns the fetch and passes the window as a data React prop (plugin-list's ListView)same

The host-prop case is why the tests pass the rows as a React prop with no data key
on the schema at all: SchemaRenderer spreads every unstripped schema key as a prop, so
a bare 'data' in props would be true of the schema's own data object too. Nothing
here tests data presence in either form; the predicate never looks.

Column-side fallbacks that keep their silence, each also counter-probed: the string[]
spelling, an absent columns, an empty columns, and hidden: true (including a hidden
column that also mis-spells its identity — it contributes nothing either way and the
author asked for nothing).

One predicate, two readers. The drop decision moved out of the inline filter into
resolvesToDataColumn, which generateColumns and the reporter now share, so the
diagnostic cannot drift from the filter that drops the column. A test pins the extracted
predicate against the inline expression it replaced, case for case.

Reverse verification

Two legs, direction predicted before running. Both ablations are directly effective: the
tests import ../ObjectGrid and ../columnSpellingDiagnostics as source, and vitest
aliases every @object-ui/* specifier to packages/*/src — no dist is involved on
either leg, and each ablation flipped results, which is itself the proof it reached the
code under test. Both were restored and the tree proved clean (git status --porcelain
empty).

Leg 1 — remove the emitter (console.warn(message) in the effect).
Predicted: the pure-module file stays fully green (it never renders); the render files
go red only where a diagnostic is asserted — 3 of 4 matrix legs, the hidden
counter-probe, all 5 row-side counter-probes (9 of 13), plus 1 of 6 in the baseline file.
Observed: Tests 10 failed | 35 passed (45) — exactly those ten, test for test.

Leg 2 — remove the hidden carve-out (hidden columns counted as unresolved).
Predicted: this direction is not "goes quiet" but more diagnostics — a false
positive over authored intent. 3 red in the unit file, 1 in the render file, baseline
untouched.
Observed: Tests 4 failed | 41 passed (45), exactly those four, and the render failure
is the predicted direction:

AssertionError: expected [ Array(1) ] to deeply equal []
- []
+ [ "[ObjectUI] ObjectGrid columns: … 1 of 1 authored columns resolve to nothing …" ]

Leg 2 found a real defect in this PR's own message and it is fixed in the second
commit: describeEntry's tail was the "field is an empty string" message with no
length check under it, so with the carve-out gone it told a well-formed column that its
non-empty field was empty. The check is now explicit and the branch below states only
what it verified. Unreachable today; a message must never assert something it did not
check.

Verification

Run on 57cda96a8 (the final commit):

  • pnpm exec vitest run packages/plugin-grid/84 files, 786 tests, all passing
  • pnpm exec vitest run packages/plugin-list/ — 43 files, 644 tests, all passing (the host that passes rows down as a prop)
  • pnpm --filter @object-ui/plugin-grid type-check — clean (tsc --noEmit && tsc -p tsconfig.test.json)
  • pnpm --filter @object-ui/plugin-grid lint — 0 errors (655 pre-existing warnings, the repo baseline; the gate is errors-only by design)
  • check:control-bytes, check:spec-symbols, check:phantom-deps, check:self-import, check:i18n-keys, check:i18n-drift, check:i18n-dead-keys, changeset:check, check-changeset-presence.mjs — all pass
  • No test skipped, disabled or quarantined. 39 tests added, 1 existing test updated.

File surface held: every source change is under packages/plugin-grid/src/**, plus the
required changeset. No new user-facing copy, so no locale pack changes — the diagnostic
is developer-facing English on the console, which is also what commandment #-1 requires
of console messages.

The one updated test

columnDeclaredSpellingOnly.test.tsx is the baseline the card said to measure against,
and its docblock claimed the drop "produces no console line". That is the sentence this
PR makes false, so it is rewritten rather than left as a stale claim. Its last case now
asserts the diagnostic and keeps pinning the escalation that was rejected: still no
throw, still no alert role. Every rendering assertion in that file is byte-identical.

Generated by Claude Code


Generated by Claude Code

os-support-aiand others added 2 commits August 20, 2026 22:54
… of silence
objectui#5068 made `ListColumnSchema`'s `field` / `label` the only column
spelling `ObjectGrid` reads, which is right — but it relocated a failure mode
instead of removing it. A column authored in a spelling the renderer does not
read contributed nothing and NOTHING SAID SO: no error, no warning, no empty
state, just a grid with its row-number column and no data columns. Renderer and
author disagree, author gets a success receipt — the same shape #5068 exists to
fix, one level down.
An authored column that can never resolve now emits one `console.warn` naming
the address rather than the symptom: which block, which object, which
`columns[i]`, the keys that entry carries, and the rewrite that works. The drop
decision moves into `resolvesToDataColumn` so the reporter and the filter that
drops the column cannot drift apart — one predicate, two readers.
The diagnostic reads the `columns` input and nothing else. It never asks whether
the grid found ROWS: `object-grid` draws them from five different places (bare
`data` array, `data.provider: 'value'`, legacy `staticData`, `bind`, and a host
that owns the fetch and hands the window down as a `data` React prop), and a
predicate consulting those would eventually paint a configuration error over a
working grid. `hidden: true` is authored intent and is never reported.
Rendering is unchanged in every measured case; the diagnostic is additive.
Fixes#5349
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
…stic
Found by this card's own reverse-verification. Ablating the `hidden` carve-out
made `describeEntry` reach its tail with a well-formed, non-empty `field` and
tell the author it was "an empty string" — the tail was the empty-string message
with no length check under it. The check is now explicit, and the branch below it
states only what it verified.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RV6yuVCxymHYE16PL9vQkE
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Main entry (gzip)25.3 KB350 KB
Entry fileindex-DeCwU2U6.js
StatusPASS

📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)7.42KB2.32KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)507.00KB113.63KB
core (index.js)4.11KB1.62KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.34KB
fields (index.js)237.21KB59.50KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)30.51KB7.57KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.75KB18.37KB
plugin-chatbot (index.js)181.21KB43.14KB
plugin-dashboard (index.js)128.37KB32.91KB
plugin-designer (index.js)212.39KB42.83KB
plugin-detail (index.js)242.04KB60.86KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)124.40KB30.26KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.75KB54.24KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.64KB27.13KB
plugin-map (index.js)20.08KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.52KB20.67KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)36.10KB12.26KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)1.45KB0.83KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-support-ai
os-support-ai marked this pull request as ready for review August 21, 2026 00:10
@os-support-ai
os-support-ai added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit c574dfbAug 21, 2026
23 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-5349-objectgrid-silent-no-columns branch August 21, 2026 00:10
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

1 participant

@os-support-ai