Skip to content

fix(plugin-dashboard): resolve column identity at the producer, before data-table (part of #5120) - #5353

Merged
os-support-ai merged 1 commit into
mainfrom
claude/issue-5120-data-table-column-identity
Aug 19, 2026
Merged

fix(plugin-dashboard): resolve column identity at the producer, before data-table (part of #5120)#5353
os-support-ai merged 1 commit into
mainfrom
claude/issue-5120-data-table-column-identity

Conversation

@os-support-ai

Copy link
Copy Markdown
Collaborator

Part of #5120

Deliberately Part of, not a closing keyword: only one of this card's two halves is here. The other half went back to the inbox because the card's own census-first fork clause tripped. Details below.

What landed — the producer half

ObjectDataTable.normalizeColumns converted the string[] shorthand and returned every object column raw. data-table is an adapter, and its column key is accessorKey — a key @object-ui/core deliberately holds outside the metadata identity fold, where column-identity.ts names it TABLE_ADAPTER_COLUMN_KEY. So a column authored in the spec-canonical spelling reached the adapter carrying no accessorKey at all, and the widget rendered a header over row[undefined]: every cell blank, nothing said. computeLookupExpand's $expand whitelist resolved c.accessorKey || c.name and missed the same column, so a field-spelled lookup also lost its related record and showed a raw FK id.

Identity is now resolved once, in the producer, through the shared columnIdentity reader, and stamped onto the adapter's own key. This is the move #5022 made in RelatedList and PR5345 (#5068) generalized in ObjectGrid, whose docblock states the rule in one line: metadata vocabulary in, adapter vocabulary out; one translation, one place.

Three rules carried over verbatim from RelatedList, each pinned:

What did NOT land, and why — the census-first fork clause tripped

The card also rules that data-table's undeclared col.name alias retires at data-table.tsx:777 / :786. That deletion is gated on a census, and the census found real authorized usage.

Two files in the published skills/ corpus — the authoring guide agents read to write ObjectUI metadata — instruct authors to spell a data-table column exactly the way this card would retire:

  • skills/objectui/guides/data-integration.md:185-192
  • skills/objectui/guides/schema-expressions.md:377-384

Both carry the identical example: a node of type data-table with "bind": "customers" and "columns": [{ "name": "name", "label": "Name" }, { "name": "email", "label": "Email" }]. That is the name limb, reaching data-table.tsx:777 directly rather than through any of the three producers. Per the clause — real authorized usage found, stop, report, inbox — the deletion is the maintainer's call, so it is not in this PR.

Census method and corpora

Corpora swept: examples/ (469 files), content/docs/ (202), apps/ (226), skills/ (29), e2e/ (33), and packages/*/README.md (39) — the READMEs included from the start, per the lesson recorded from #5068's sweep.

Three independent methods, each counter-probed with a term known present, through that same method:

Method What it does Control Result
1. JSON structural walk parses every .json, walks to each columns array, records the enclosing node's type, classifies each entry by identity key accessorKey25 entries found ✅ control passed
2. Bracket-matched text extraction balances columns: [ … ] in .ts/.tsx/.md/.mdx/.yml, splits top-level entries, records nearest preceding type accessorKey11 + 10 entries; field34 + 22 ✅ control passed
3. Type-anchored window grep anchors on each data-table-family type token, reads the next 25 lines, reports spellings present found all 5 known accessorKey example JSONs and the README ✅ control passed

No method was discarded — all three controls held. Method 3 produced three false positives that method 2 correctly excluded (name: keys belonging to data rows, not columns, at content/docs/guide/quick-start.md:83, content/docs/index.md:23, examples/.../simple-table.json), which is the cross-validation earning its keep. The 4 unparseable JSON files are tsconfig*.json (JSONC with comments); none contains a columns array.

Every hit was classified by the enclosing node's type, as the card requires. Everything below the data-table row is a different component's own vocabulary and never reaches this adapter:

Enclosing type Verdict
data-table 🛑 2 hits — real authorized usage (the two skills guides above)
object-data-table zero authored usages in any corpus — only registry tables in plugin-dashboard.mdx, the package README, and register-plugins.ts
grid (form field) authorized, and name is its declared spelling — GridColumnDefinition declares name, which is #3951's outcome; the form layer means the opposite thing by these key names, as column-identity.ts says outright
table (static renderer) its own renderer, not this adapter; its docs actively declare name/label — filed as #5350
crud documented at schema-reference.md:537; no crud renderer is registered in this repo (types, zod schema, validator branch and builder exist; no ComponentRegistry.register). Reported as an unverified observation, not filed
list ListView folds name to field via normalizeColumnIdentities, then emits object-grid, where ObjectGrid writes accessorKey on the way out — never touches the alias
object-grid docs and skills teach { name, label } here and ObjectGrid silently drops it, before and after #5068 — filed as #5352
object-master-detail-form, exportExcelWithFormulas form / export-column vocabularies, each declaring name itself

The measurement that matters for the decision

The held deletion was ablated to price it: both alias sites cut, then packages/components (the adapter's own package) and packages/plugin-dashboard run together.

234 test files, 2122 tests, all green. Not one test in the repo pins the limb. A repo-wide scan of the source and test corpus finds exactly one name-spelled column on a data-table-family node — packages/core/src/validation/__tests__/schema-validator.test.ts:26, a crud schema in a validator test, never rendered.

So the retirement is invisible to CI. Had the census been skipped, it would have merged green and silently broken the column vocabulary the shipped skills guides teach. The census is the only thing standing in front of it.

Reverse verification — predicted before each run, then observed

No build artifact sits between any edit and the thing under test, on any leg: the root vitest.config.mts (L245-261) aliases every workspace package — @object-ui/core, @object-ui/components, @object-ui/plugin-dashboard — to that package's src/, so vitest transforms source directly and no dist/ is in the path. Checked before the legs were run, not assumed.

Leg A — producer-only revert (ObjectDataTable.tsx restored to origin/main, tests kept).
Predicted red: the two identity-stamp tests, the field $expand test, the field seam render, and the legibility pin (because { field: … } becomes unresolvable there too). Predicted green: the other seven.
Observed: exactly those 5 red, 7 green — every name matched.

Leg A, second half — the silent-regression check. Predicted: the pre-existing suite stays fully green.
Observed: 65 of 66 files and 601 of 606 tests green, the only red being this PR's new file. The same shape #5068 measured: three ObjectDataTable test files already existed and every one of them authors its columns accessorKey-spelled, so the defect was invisible to all of them.

Leg B — the held half, ablated forward, together with this PR's change (both alias sites cut, producer change in place).
Predicted: nothing goes red, and this PR's 12 tests stay green.
Observed: 234 files / 2122 tests green. The second half of that prediction is the load-bearing one — it shows this change is alias-independent: it survives whichever way the maintainer rules, and it is what stops object-data-table from depending on the alias at all. That makes it a prerequisite for the deletion, not a substitute for it.

Legibility, measured rather than assumed

The card asks what a now-unresolvable column does. It is not dropped and does not throw: the header renders over empty cells and the neighbouring column is unaffected. It is also not quite silent, and the noise is the interesting part — the adapter keys each cell by the accessor (key={col.accessorKey}, data-table.tsx:1829), so an unresolved column hands React undefined and React emits its generic missing-key warning. That warning names tr and DataTableRenderer; it names neither the column nor the metadata that produced it. Nothing is said in ObjectUI's own voice.

Unchanged by this PR — such a column carried no accessorKey before it either — and pinned as behaviour so #5349 has something to measure against. No diagnostic is implemented here, per the dispatch.

Tests

Run from the repo root (a package-scoped run would use a different config than CI does), at final HEAD a3920c3:

  • pnpm exec vitest run packages/plugin-dashboard/src/__tests__/ObjectDataTable.columnIdentity.test.tsx12 passed
  • pnpm exec vitest run packages/plugin-dashboard --maxWorkers=266 files, 606 tests passed
  • pnpm --filter @object-ui/plugin-dashboard type-check → clean (after pnpm --filter '@object-ui/plugin-dashboard^...' build; the dependency closure has to exist first or tsc reports every workspace import as a missing module)
  • pnpm --filter @object-ui/plugin-dashboard lint → 0 errors (321 pre-existing warnings)
  • check:control-bytes, check:phantom-deps, check:self-import → all OK

Findings filed along the way

All unassigned, finding class, none of them touched here: #5350, #5351, #5352.


Generated by Claude Code

…e data-table

`ObjectDataTable.normalizeColumns` returned object columns raw, so a column
authored in the spec-canonical spelling (`{ field: 'stage' }`) reached the
data-table adapter with no `accessorKey` and rendered a header over
`row[undefined]`. `computeLookupExpand` missed the same column for the same
reason, dropping `field`-spelled lookups out of `$expand`.

Identity is now resolved once, here, via the shared `columnIdentity` reader and
stamped onto `accessorKey` — the adapter's own key, which `@object-ui/core`
deliberately holds outside the metadata identity fold. Metadata vocabulary in,
adapter vocabulary out; one translation, one place (objectui#5022, #5068).

An author-supplied `accessorKey` is never overwritten, an already-canonical
entry is returned by reference, and an entry with no resolvable identity is
returned untouched.

Part of #5120 — the consumer half (retiring data-table's undeclared `col.name`
alias) is held pending the maintainer's ruling; the census found real authorized
usage of that spelling in the published skills guides.

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

Metric Value Budget
Main entry (gzip) 25.3 KB 350 KB
Entry file index-DgmnbPIG.js
Status PASS

📦 Bundle Size Report

Package Size Gzipped
app-shell (index.js) 9.83KB 3.70KB
app-shell (runtime-config.js) 7.42KB 2.32KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 8.92KB 3.41KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 1.17KB 0.53KB
auth (AuthProvider.js) 29.33KB 7.05KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.13KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.64KB 2.21KB
auth (SocialSignInButtons.js) 9.60KB 3.89KB
auth (UserMenu.js) 3.40KB 1.22KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.79KB
auth (createAuthenticatedFetch.js) 6.34KB 2.43KB
auth (index.js) 2.71KB 1.22KB
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.02KB 0.88KB
auth (useIsWorkspaceAdmin.js) 1.61KB 0.85KB
collaboration (CommentThread.js) 26.07KB 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.65KB 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) 505.53KB 113.13KB
core (index.js) 4.11KB 1.62KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 159.80KB 44.34KB
fields (index.js) 237.07KB 59.46KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.42KB 1.39KB
i18n (pickLocalized.js) 3.69KB 1.73KB
i18n (provider.js) 23.13KB 7.63KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 30.51KB 7.57KB
i18n (useSafeTranslation.js) 7.77KB 3.13KB
layout (index.js) 38.75KB 10.87KB
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.74KB
mobile (index.js) 1.50KB 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.71KB 0.42KB
mobile (useResponsiveConfig.js) 1.36KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 9.35KB 3.31KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 4.42KB 1.42KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.91KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.52KB
permissions (usePermissions.js) 1.81KB 0.83KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.62KB 12.83KB
plugin-charts (index.js) 64.75KB 18.37KB
plugin-chatbot (index.js) 181.21KB 43.14KB
plugin-dashboard (index.js) 128.07KB 32.77KB
plugin-designer (index.js) 212.39KB 42.83KB
plugin-detail (index.js) 241.46KB 60.56KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 124.19KB 30.20KB
plugin-gantt (index.js) 164.10KB 39.87KB
plugin-grid (index.js) 197.30KB 53.06KB
plugin-kanban (index.js) 52.93KB 14.60KB
plugin-list (index.js) 111.66KB 27.13KB
plugin-map (index.js) 20.08KB 6.62KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.49KB 11.93KB
plugin-timeline (index.js) 26.68KB 7.66KB
plugin-tree (index.js) 8.50KB 2.88KB
plugin-view (index.js) 84.52KB 20.67KB
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.44KB 0.22KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 3.77KB 1.33KB
react (SchemaRenderer.js) 36.10KB 12.26KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 1.33KB 0.69KB
react (schema-input.js) 1.45KB 0.83KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (index.js) 4.77KB 2.16KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 10.76KB 3.17KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 6.92KB 2.40KB
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) 0.20KB 0.18KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 0.20KB 0.18KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.87KB 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-retry.js) 4.32KB 2.02KB
types (index.js) 3.08KB 1.53KB
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 (system-fields.js) 3.33KB 1.54KB
types (theme.js) 0.20KB 0.18KB
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-support-ai
os-support-ai marked this pull request as ready for review August 19, 2026 19:40

Copy link
Copy Markdown
Collaborator Author

ACCEPT — PM review, round 17. Part of #5120, and the card stays in the decision box: the consumer half tripped the census fork clause and is routed to the maintainer.

  • Path surface: 3 files — packages/plugin-dashboard/src/ObjectDataTable.tsx, one new test, one changeset. Zero governed-surface hits. packages/components/src/renderers/complex/data-table.tsx is deliberately untouched.
  • Gates: every gate job completed: success — Lint, Type Check, Test shards 1–4, Build & E2E, Build Docs, Doc Snippet / Doc Component Type Check, Changeset Declaration / Bump Policy / Fixed Group, Bundle Analysis, Control Byte Scan, Internal Docs Link Check, Skill Guide Path Check. No cancelled, no in_progress.

Both deviations from my dispatch text are forced, and both are right

Part of rather than Fixes — a closing keyword on a card headed for the decision box would silently close it on merge. One changeset rather than two — only plugin-dashboard was touched. Neither needed asking; both were reported.

What it delivers, and why it stands whichever way the fork is ruled

normalizeColumns resolves column identity through the shared columnIdentity reader and stamps it onto accessorKey; computeLookupExpand reads the same identity rather than c.accessorKey || c.name. A field-spelled column on object-data-table now renders its cells and, when relational, enters $expandboth were empty before.

Measured alias-independent (leg B: both alias sites cut → 234 files / 2122 tests green), so it is a prerequisite for the deletion rather than a substitute: it is what stops object-data-table depending on the alias at all.

The leg that matters most, and it is not the one that went red

Leg A predicted 5 red / 7 green by name and observed exactly that. Then the second half: with the producer reverted, the pre-existing suite stayed greenTest Files 1 failed | 65 passed (66), the only failure being the new file. Three ObjectDataTable test files already existed and every one authors its columns accessorKey-spelled, so the defect was invisible to all of them. Same shape #5068 measured one package over: the suite that should have caught it was written in the vocabulary that hides it.

The census, done properly

Six corpora (examples/ 469, content/docs/ 202, apps/ 226, skills/ 29, e2e/ 33, packages/*/README.md 39 — READMEs included from the start, per the #5068 lesson), three methods, each counter-probed through its own method with a term known present, and all three controls passed, so no method was discarded this time. Method 3 threw 3 false positives that method 2 correctly excluded (name: keys in data rows, not columns). Hits classified by enclosing node type; object-data-table shows zero authored name usages anywhere.

And the finding that stopped the deletion: skills/objectui/guides/data-integration.md:185-192 and schema-expressions.md:377-384 teach data-table columns spelled { name, label }. Real authorized usage, in the AI-authoring instruction set — the one corpus no gate reads.

Legibility, measured rather than assumed

An unresolvable column is not dropped and does not throw — header over empty cells, neighbour unaffected. Not quite silent either, and that is worse: the adapter keys each cell by the accessor (data-table.tsx:1829), so React emits its generic missing-key warning naming tr and DataTableRenderer — never the column, never the metadata. Nothing in ObjectUI's own voice. Pinned as behaviour, no diagnostic implemented (correctly — that is #5349's scope, and I said so).

Three findings filed rather than folded in: #5350 (the static table renderer carries the same alias, and its published docs declare name/label while packages/types declares header/accessorKey), #5351 (the col.label alias on the same two lines), #5352 (docs and skills teach {name,label} on a directly-authored object-gridverified identical before and after #5068, so explicitly not a PR5345 regression; that check is the part I want to credit).

Merging via the queue. #5120 stays open in the decision box — Q1 (retirement order vs. the corpus that teaches it, decided together with #5350) and Q2 (col.label) are with the maintainer.


Generated by Claude Code

@os-support-ai
os-support-ai added this pull request to the merge queue Aug 19, 2026
Merged via the queue into main with commit 58398ba Aug 19, 2026
22 checks passed
@os-support-ai
os-support-ai deleted the claude/issue-5120-data-table-column-identity branch August 19, 2026 19:41
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.

1 participant