Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .changeset/5120-retire-data-table-name-alias.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
---
'@object-ui/components': minor
---

**Breaking for authored metadata:** a `data-table` column spelled `name` no
longer resolves its cells. Use the declared `accessorKey`.

`data-table`'s column normalization used to read
`accessorKey: col.accessorKey || col.name` — but `TableColumn`
(`@object-ui/types`) declares only `accessorKey`, never `name`. The declared
surface admitted one spelling while the runtime admitted two, which is the
second de-facto contract AGENTS.md #0.1 forbids. The maintainer ruling of
2026-08-20 settled the direction for the whole family: retire the consumer-side
alias, translate at the producers. `label` → `header` retired first
(objectui#5351); this retires `name` → `accessorKey` and closes the family.

**Who is affected — a column authored DIRECTLY onto a `data-table` node:**

```json
{ "type": "data-table",
"columns": [{ "name": "email", "label": "Email" }] } // ← was tolerated
```

becomes

```json
{ "type": "data-table",
"columns": [{ "header": "Email", "accessorKey": "email" }] }
```

**Who is NOT affected.** Columns reaching the table through `object-data-table`,
a detail view's `related[]` list, or `object-grid` are unchanged — the adapter
never sees a legacy spelling from any of them. The reason differs by producer,
and the difference matters if you are debugging one:

- `object-data-table` and a detail view's `related[]` list **resolve** the
legacy spelling before delivery, stamping `accessorKey` from `name` (via
`columnIdentity`). A `name`-spelled column keeps working there.
- `object-grid` **refuses** it instead: since objectui#5068 an authored column
must spell the declared `field`, and one that does not is dropped at intake
and never reaches the table. Its delivered columns carry `accessorKey`
stamped from `field`. So a `name`-spelled `object-grid` column does not
render today either — that is objectui#5352's open question, unchanged by
this release.

Only the directly-authored `data-table` node narrows here.

**How the break presents, so you can recognise it:** the column is not dropped
and nothing is thrown — its header still renders over blank cells, and
neighbouring columns are unaffected. If a table's header row looks right but one
column's cells are empty, check that column's key spelling first.

The two published skill guides that taught the `name` spelling
(`skills/objectui/guides/data-integration.md`, `schema-expressions.md`) migrate
in this same release, so the platform never refuses a spelling it still ships.

Graded `minor`, not `patch`: this narrows the accepted input set, which is a
breaking change for any author who used the tolerated spelling. It is not
`major` per this repo's fixed-group convention (objectui's own breaking changes
ship as `minor`; the group's major tracks `@objectstack`).
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,11 +40,24 @@
* empty state. The correction is verified against the renderer rather
* than against a reading of it.
*
* The `columns` entries in these examples are deliberately untouched: their
* `{ name, label }` spelling is the separate open question on objectui#5120
* (the undeclared `col.name` / `col.label` alias at `data-table.tsx:776-777`),
* parked with the maintainer. This file pins the BINDING only, and stays true
* whichever way that one lands — nothing below asserts a column key spelling.
* ⚠️ COLUMN SPELLING — this file is NOT spelling-independent, and an earlier
* revision of this docblock said it was. It claimed the file "pins the BINDING
* only … nothing below asserts a column key spelling". That was false and was
* filed as objectui#5479: the BEHAVIOUR assertions below are on rendered CELL
* TEXT, and a cell only has text when its column's accessor resolves — so they
* transitively pin the accessor spelling the guides use. A docblock asserting
* independence over assertions that were not independent is what made
* objectui#5120's last step invisible when the family was ruled on.
*
* Both aliases have since retired — `label` in objectui#5351, `name` in
* objectui#5120 — and the two guides' `data-table` columns migrated to the
* declared `{ header, accessorKey }` in the same commit as the `name`
* retirement, because these blocks are lifted and rendered at run time and
* would otherwise go blank. That coupling is the point, not a defect: the
* guides are executable fixtures, so the instruction corpus cannot drift from
* the runtime without this file going red. Keep it that way — if a future
* change makes the adapter's accepted key set narrower again, migrate the
* guides in the SAME commit.
*/

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
Expand DownExpand Up@@ -162,6 +175,31 @@ describe('skill guides — no `data-table` example is bound with `bind` (#5126,
expect(offenders).toEqual([]);
});

it('no `data-table` example spells a column with the retired `name` (#5120)', () => {
// objectui#5120 retired `accessorKey: col.accessorKey || col.name` on the
// adapter. These blocks are RENDERED below, so a guide that regressed to
// `name` would already fail — but only as "expected [] to equal
// ['Ada Lovelace', …]", which names neither the key nor the card. This
// assertion is here to make the failure SAY what broke, and to hold the
// corpus even if the behaviour legs are ever narrowed.
for (const rel of GUIDE_PATHS) {
const md = readGuide(rel);
for (const node of blocksOfType(md, 'data-table')) {
const columns = (node.columns ?? []) as Record<string, unknown>[];
for (const col of columns) {
expect(
'name' in col,
`${rel}: a data-table column spells the retired \`name\`; the declared key is \`accessorKey\` (objectui#5120)`,
).toBe(false);
expect(
'accessorKey' in col,
`${rel}: a data-table column is missing the declared \`accessorKey\` (objectui#5120)`,
).toBe(true);
}
}
}
});

it('no guide claims a table component calls useDataScope', () => {
for (const rel of GUIDE_PATHS) {
const md = readGuide(rel);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,9 +57,16 @@ const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, '../../../..');
const skillsRoot = path.join(repoRoot, 'skills/objectui');

// The DECLARED column keys (`TableColumn`: `header` + `accessorKey`). These
// mirror the two published guides' `data-table` example, which is why they
// moved when the guides did: objectui#5120 retired the adapter's undeclared
// `col.name` alias, so the `{ name, label }` spelling this fixture used to
// carry now resolves no accessor and every cell below would read ''. The rows
// keep `name`/`email` as their own DATA keys — that is what `accessorKey`
// points AT, and it is unrelated to the column vocabulary.
const COLUMNS = [
{ name: 'name', label: 'Name' },
{ name: 'email', label: 'Email' },
{ header: 'Name', accessorKey: 'name' },
{ header: 'Email', accessorKey: 'email' },
];
const ROWS = [
{ name: 'Ada Lovelace', email: 'ada@example.com' },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,8 +7,9 @@
*/

/**
* `data-table` reads the DECLARED `header`, not the undeclared `label`
* (objectui#5351).
* `data-table` reads the DECLARED `header` and `accessorKey`, and NOTHING else
* — not the undeclared `label` (objectui#5351), not the undeclared `name`
* (objectui#5120).
*
* `TableColumn` (`packages/types/src/data-display.ts`) declares `header: string`
* and `accessorKey: string`. It declares neither `label` nor `name`. The
Expand All@@ -26,14 +27,39 @@
* purpose. Metadata vocabulary in, adapter vocabulary out; one translation, one
* place — and that place is each producer, never here.
*
* SCOPE. The `label` alias retires here; the `name` alias is HELD, and the last
* describe below pins it as still-read so the hold cannot be mistaken for the
* retirement having happened. Two published skill guides teach a directly
* authored `data-table` whose columns are spelled `{ name, label }`, and
* SCOPE. `label` retired in objectui#5351; `name` retires here, closing the
* family. The hold that kept `name` alive was never about the producers — all
* three resolve `accessorKey` themselves, measured — but about the INSTRUCTION
* CORPUS: two published skill guides taught a directly authored `data-table`
* whose columns were spelled `{ name, label }`, and
* `skill-guide-data-table-binding.test.tsx` renders those blocks straight out of
* the guide files — so `name` cannot retire until the instruction corpus moves.
* That is objectui#5120's remaining step and is nobody's to take unbidden:
* `skills/**` is a customer-published surface with its own owning seat.
* the guide files. Both guides migrated to `{ header, accessorKey }` in the same
* commit as this retirement, which is the only ordering in which the platform
* never refuses a spelling it still ships.
*
* WHAT NARROWS, precisely. A `{ name, label }` column arriving through
* `ObjectDataTable`, `RelatedList` or `ObjectGrid` is UNAFFECTED — but for two
* different reasons, and conflating them is a mistake this card's own ruling
* already made once:
*
* - `ObjectDataTable` and `RelatedList` RESOLVE the legacy spelling, stamping
* `accessorKey` from `columnIdentity(col)` before delivery.
* - `ObjectGrid` does NOT fold. Since objectui#5068 it REFUSES undeclared
* spellings at intake — `resolvesToDataColumn` requires a string `field`
* (`columnSpellingDiagnostics.ts`), so a `name`-spelled entry is dropped
* before delivery and never reaches this adapter at all (before AND after
* this change; that silence is objectui#5352's territory). What it does
* deliver carries `accessorKey` stamped from `field`.
*
* The 2026-08-20 ruling's item 2 told ObjectGrid to "connect to the same
* `columnIdentity` resolution"; objectui#5478 measured that following it would
* have RE-WIDENED what #5068 narrowed. Stated here because the claim has been
* restated wrongly more than once, and a third repetition would settle it.
*
* Either way the adapter never sees a legacy spelling from a producer. What
* stops resolving is `name` on a column authored DIRECTLY onto a `data-table`
* node. That is the whole blast radius, and it is pinned below in both
* directions.
*
* The two aliases were DIFFERENT failure classes, which is why the cards were
* filed apart: an unresolved `accessorKey` gives blank cells under a live
Expand DownExpand Up@@ -79,24 +105,52 @@ describe('data-table columns — the declared keys render (unchanged)', () => {
});
});

describe('data-table columns — the `name` alias is still read, and that is a HOLD (#5120)', () => {
it('still resolves an accessor from the undeclared `name`', () => {
// NOT an endorsement — a receipt. The 2026-08-20 ruling retires this limb;
// what stops it today is that two published skill guides teach it and
// `skill-guide-data-table-binding.test.tsx` renders their bytes. Pinning the
// CURRENT behaviour means the day the guides move, this test goes red and
// names itself as the thing to delete, instead of the retirement quietly
// never happening.
describe('data-table columns — the undeclared `name` alias is retired (#5120)', () => {
it('does not resolve an accessor from `name`', () => {
// The narrowing this card ships, and the receipt that replaces the HOLD pin
// that stood here while the instruction corpus still taught `name`.
// A DIFFERENT failure class from #5351's: the header is fine and the CELLS
// are what go blank.
renderTable([{ header: 'Stage', name: 'stage' }]);
expect(bodyCells()).toEqual(['Won', 'Lost']);
expect(headers()).toEqual(['Stage']);
expect(bodyCells()).toEqual(['', '']);
});

it('keeps an authored `accessorKey` ahead of a divergent `name`', () => {
it('keeps an authored `accessorKey` winning over a divergent `name`', () => {
// Precedence, unchanged and load bearing: columns can arrive in the table
// library's own shape, and those must not be second-guessed.
// library's own shape, and those must not be second-guessed. This passed
// before the retirement too — it is here so the two directions cannot drift.
renderTable([{ header: 'Stage', accessorKey: 'stage', name: 'nonsense' }]);
expect(bodyCells()).toEqual(['Won', 'Lost']);
});

it('LEGIBILITY: an unresolvable column keeps its header and spares its neighbour', () => {
// Measured, not assumed, and deliberately pinned as the SHAPE OF THE BREAK:
// the column is not dropped and nothing throws — a live header sits over
// blank cells while the declared neighbour is untouched. This is the failure
// an author who kept the legacy spelling now gets, and it is the same
// illegible shape objectui#5349 measures against.
renderTable([
{ header: 'Stage', name: 'stage' },
{ header: 'Id', accessorKey: 'id' },
]);
expect(headers()).toEqual(['Stage', 'Id']);
expect(bodyCells()).toEqual(['', '1', '', '2']);
});

it('a PRODUCER-resolved column is unaffected — the narrowing is adapter-only', () => {
// The blast-radius bound in one assertion: whatever a producer delivers
// already carries a declared `accessorKey`, so the retirement cannot reach
// it. ObjectDataTable and RelatedList get there by RESOLVING `name` through
// `columnIdentity`; ObjectGrid gets there by REFUSING undeclared spellings
// at intake and stamping from `field` (see the header — the two routes are
// not the same mechanism). Simulating the hand-off here keeps this file
// honest about what the retirement does and does not break, without
// importing a plugin package.
renderTable([{ header: 'Stage', name: 'stage', accessorKey: 'stage' }]);
expect(headers()).toEqual(['Stage']);
expect(bodyCells()).toEqual(['Won', 'Lost']);
});
});

describe('data-table columns — the undeclared `label` alias is retired (#5351)', () => {
Expand Down
41 changes: 23 additions & 18 deletions packages/components/src/renderers/complex/data-table.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -832,9 +832,9 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
if (nonArrayDataMessage) console.warn(nonArrayDataMessage);
}, [nonArrayDataMessage]);

// The adapter reads the column keys `TableColumn` DECLARES. The `label`
// alias is gone (objectui#5351); the `name` alias is HELD, and the hold is
// deliberate and documented rather than an oversight.
// The adapter reads ONLY the column keys `TableColumn` DECLARES. Both
// undeclared aliases are now retired: `label` (objectui#5351) and `name`
// (objectui#5120, this change).
//
// These two lines used to normalize each column as
// `header: col.header || col.label` and
Expand All@@ -846,28 +846,33 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
// ruling settled the direction for the whole family: retire the consumer-side
// alias, unify the producers.
//
// `header` has retired. Where its translation went — `columnHeader` in
// `@object-ui/core`, called by each producer before delivery:
// Where the translation went — `columnIdentity` / `columnHeader` in
// `@object-ui/core`, called by each producer BEFORE delivery:
// `ObjectDataTable.normalizeColumns` (`@object-ui/plugin-dashboard`)
// `RelatedList.normalizeColumn` (`@object-ui/plugin-detail`)
// `ObjectGrid.generateColumns` (`@object-ui/plugin-grid`, since #5068)
// Metadata vocabulary in, adapter vocabulary out; one translation, one place.
// Metadata vocabulary in, adapter vocabulary out; one translation, one place —
// and that place is each producer, never here. A `{ name, label }` column
// arriving through any of the three still renders: its producer resolved the
// identity into `accessorKey` before the adapter ever saw it. What no longer
// resolves is `name` on a column authored DIRECTLY onto a `data-table` node,
// which is the accepted-set narrowing objectui#5120 rules and ships.
//
// `accessorKey || col.name` STAYS, pending objectui#5120's remaining step. It
// is not that the producers still need it — all three resolve `accessorKey`
// themselves, measured — but that two PUBLISHED skill guides teach a directly
// authored `data-table` whose columns are spelled `{ name, label }`:
// The instruction corpus moved in the SAME commit, which is the whole reason
// this step could be taken: `skill-guide-data-table-binding.test.tsx` lifts the
// fenced JSON out of the published guides at run time and renders it, so the
// guides are executable fixtures rather than prose. Both now teach
// `{ header, accessorKey }`:
// skills/objectui/guides/data-integration.md
// skills/objectui/guides/schema-expressions.md
// `skill-guide-data-table-binding.test.tsx` lifts those blocks out of the real
// files at run time and renders them, so retiring `name` here turns that gate
// red until the guides move. Retiring the runtime ahead of the instruction
// would leave the platform refusing a spelling it still ships, and the failure
// it teaches into is the illegible one below: a header over blank cells.
// Retiring the runtime ahead of the instruction would have left the platform
// refusing a spelling it still shipped, and the failure it teaches into is the
// illegible one: a header over blank cells (pinned in
// `data-table-declared-column-keys.test.tsx`).
const initialColumns = useMemo(() => {
return rawColumns.map((col: any) => ({
...col,
accessorKey: col.accessorKey || col.name,
accessorKey: col.accessorKey,
}));
}, [rawColumns]);

Expand All@@ -876,10 +881,10 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
const widths: Record<string, number> = {};
// Spelled identically to `initialColumns` above — the auto-width pass must
// measure the SAME columns the table renders, so the two reads move
// together (objectui#5351 retired `header`'s alias; `name`'s is held).
// together (objectui#5351 retired `header`'s alias, objectui#5120 `name`'s).
const cols = rawColumns.map((col: any) => ({
header: col.header,
accessorKey: col.accessorKey || col.name,
accessorKey: col.accessorKey,
width: col.width,
fitContent: col.fitContent,
}));
Expand Down
4 changes: 2 additions & 2 deletions skills/objectui/guides/data-integration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,8 +217,8 @@ through is measured, with its open-question caveat, in
{ "name": "Grace Hopper", "email": "grace@example.com" }
],
"columns": [
{ "name": "name", "label": "Name" },
{ "name": "email", "label": "Email" }
{ "header": "Name", "accessorKey": "name" },
{ "header": "Email", "accessorKey": "email" }
]
}
```
Expand Down
4 changes: 2 additions & 2 deletions skills/objectui/guides/schema-expressions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -421,8 +421,8 @@ and is blank is the whole failure.
{ "name": "Grace Hopper", "email": "grace@example.com" }
],
"columns": [
{ "name": "name", "label": "Name" },
{ "name": "email", "label": "Email" }
{ "header": "Name", "accessorKey": "name" },
{ "header": "Email", "accessorKey": "email" }
]
}
```
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .changeset/5120-retire-data-table-name-alias.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
---
'@object-ui/components': minor
---

**Breaking for authored metadata:** a `data-table` column spelled `name` no
longer resolves its cells. Use the declared `accessorKey`.

`data-table`'s column normalization used to read
`accessorKey: col.accessorKey || col.name` — but `TableColumn`
(`@object-ui/types`) declares only `accessorKey`, never `name`. The declared
surface admitted one spelling while the runtime admitted two, which is the
second de-facto contract AGENTS.md #0.1 forbids. The maintainer ruling of
2026-08-20 settled the direction for the whole family: retire the consumer-side
alias, translate at the producers. `label` → `header` retired first
(objectui#5351); this retires `name` → `accessorKey` and closes the family.

**Who is affected — a column authored DIRECTLY onto a `data-table` node:**

```json
{ "type": "data-table",
"columns": [{ "name": "email", "label": "Email" }] } // ← was tolerated
```

becomes

```json
{ "type": "data-table",
"columns": [{ "header": "Email", "accessorKey": "email" }] }
```

**Who is NOT affected.** Columns reaching the table through `object-data-table`,
a detail view's `related[]` list, or `object-grid` are unchanged — the adapter
never sees a legacy spelling from any of them. The reason differs by producer,
and the difference matters if you are debugging one:

- `object-data-table` and a detail view's `related[]` list **resolve** the
legacy spelling before delivery, stamping `accessorKey` from `name` (via
`columnIdentity`). A `name`-spelled column keeps working there.
- `object-grid` **refuses** it instead: since objectui#5068 an authored column
must spell the declared `field`, and one that does not is dropped at intake
and never reaches the table. Its delivered columns carry `accessorKey`
stamped from `field`. So a `name`-spelled `object-grid` column does not
render today either — that is objectui#5352's open question, unchanged by
this release.

Only the directly-authored `data-table` node narrows here.

**How the break presents, so you can recognise it:** the column is not dropped
and nothing is thrown — its header still renders over blank cells, and
neighbouring columns are unaffected. If a table's header row looks right but one
column's cells are empty, check that column's key spelling first.

The two published skill guides that taught the `name` spelling
(`skills/objectui/guides/data-integration.md`, `schema-expressions.md`) migrate
in this same release, so the platform never refuses a spelling it still ships.

Graded `minor`, not `patch`: this narrows the accepted input set, which is a
breaking change for any author who used the tolerated spelling. It is not
`major` per this repo's fixed-group convention (objectui's own breaking changes
ship as `minor`; the group's major tracks `@objectstack`).
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,11 +40,24 @@
* empty state. The correction is verified against the renderer rather
* than against a reading of it.
*
* The `columns` entries in these examples are deliberately untouched: their
* `{ name, label }` spelling is the separate open question on objectui#5120
* (the undeclared `col.name` / `col.label` alias at `data-table.tsx:776-777`),
* parked with the maintainer. This file pins the BINDING only, and stays true
* whichever way that one lands — nothing below asserts a column key spelling.
* ⚠️ COLUMN SPELLING — this file is NOT spelling-independent, and an earlier
* revision of this docblock said it was. It claimed the file "pins the BINDING
* only … nothing below asserts a column key spelling". That was false and was
* filed as objectui#5479: the BEHAVIOUR assertions below are on rendered CELL
* TEXT, and a cell only has text when its column's accessor resolves — so they
* transitively pin the accessor spelling the guides use. A docblock asserting
* independence over assertions that were not independent is what made
* objectui#5120's last step invisible when the family was ruled on.
*
* Both aliases have since retired — `label` in objectui#5351, `name` in
* objectui#5120 — and the two guides' `data-table` columns migrated to the
* declared `{ header, accessorKey }` in the same commit as the `name`
* retirement, because these blocks are lifted and rendered at run time and
* would otherwise go blank. That coupling is the point, not a defect: the
* guides are executable fixtures, so the instruction corpus cannot drift from
* the runtime without this file going red. Keep it that way — if a future
* change makes the adapter's accepted key set narrower again, migrate the
* guides in the SAME commit.
*/

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
Expand DownExpand Up@@ -162,6 +175,31 @@ describe('skill guides — no `data-table` example is bound with `bind` (#5126,
expect(offenders).toEqual([]);
});

it('no `data-table` example spells a column with the retired `name` (#5120)', () => {
// objectui#5120 retired `accessorKey: col.accessorKey || col.name` on the
// adapter. These blocks are RENDERED below, so a guide that regressed to
// `name` would already fail — but only as "expected [] to equal
// ['Ada Lovelace', …]", which names neither the key nor the card. This
// assertion is here to make the failure SAY what broke, and to hold the
// corpus even if the behaviour legs are ever narrowed.
for (const rel of GUIDE_PATHS) {
const md = readGuide(rel);
for (const node of blocksOfType(md, 'data-table')) {
const columns = (node.columns ?? []) as Record<string, unknown>[];
for (const col of columns) {
expect(
'name' in col,
`${rel}: a data-table column spells the retired \`name\`; the declared key is \`accessorKey\` (objectui#5120)`,
).toBe(false);
expect(
'accessorKey' in col,
`${rel}: a data-table column is missing the declared \`accessorKey\` (objectui#5120)`,
).toBe(true);
}
}
}
});

it('no guide claims a table component calls useDataScope', () => {
for (const rel of GUIDE_PATHS) {
const md = readGuide(rel);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,9 +57,16 @@ const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, '../../../..');
const skillsRoot = path.join(repoRoot, 'skills/objectui');

// The DECLARED column keys (`TableColumn`: `header` + `accessorKey`). These
// mirror the two published guides' `data-table` example, which is why they
// moved when the guides did: objectui#5120 retired the adapter's undeclared
// `col.name` alias, so the `{ name, label }` spelling this fixture used to
// carry now resolves no accessor and every cell below would read ''. The rows
// keep `name`/`email` as their own DATA keys — that is what `accessorKey`
// points AT, and it is unrelated to the column vocabulary.
const COLUMNS = [
{ name: 'name', label: 'Name' },
{ name: 'email', label: 'Email' },
{ header: 'Name', accessorKey: 'name' },
{ header: 'Email', accessorKey: 'email' },
];
const ROWS = [
{ name: 'Ada Lovelace', email: 'ada@example.com' },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,8 +7,9 @@
*/

/**
* `data-table` reads the DECLARED `header`, not the undeclared `label`
* (objectui#5351).
* `data-table` reads the DECLARED `header` and `accessorKey`, and NOTHING else
* — not the undeclared `label` (objectui#5351), not the undeclared `name`
* (objectui#5120).
*
* `TableColumn` (`packages/types/src/data-display.ts`) declares `header: string`
* and `accessorKey: string`. It declares neither `label` nor `name`. The
Expand All@@ -26,14 +27,39 @@
* purpose. Metadata vocabulary in, adapter vocabulary out; one translation, one
* place — and that place is each producer, never here.
*
* SCOPE. The `label` alias retires here; the `name` alias is HELD, and the last
* describe below pins it as still-read so the hold cannot be mistaken for the
* retirement having happened. Two published skill guides teach a directly
* authored `data-table` whose columns are spelled `{ name, label }`, and
* SCOPE. `label` retired in objectui#5351; `name` retires here, closing the
* family. The hold that kept `name` alive was never about the producers — all
* three resolve `accessorKey` themselves, measured — but about the INSTRUCTION
* CORPUS: two published skill guides taught a directly authored `data-table`
* whose columns were spelled `{ name, label }`, and
* `skill-guide-data-table-binding.test.tsx` renders those blocks straight out of
* the guide files — so `name` cannot retire until the instruction corpus moves.
* That is objectui#5120's remaining step and is nobody's to take unbidden:
* `skills/**` is a customer-published surface with its own owning seat.
* the guide files. Both guides migrated to `{ header, accessorKey }` in the same
* commit as this retirement, which is the only ordering in which the platform
* never refuses a spelling it still ships.
*
* WHAT NARROWS, precisely. A `{ name, label }` column arriving through
* `ObjectDataTable`, `RelatedList` or `ObjectGrid` is UNAFFECTED — but for two
* different reasons, and conflating them is a mistake this card's own ruling
* already made once:
*
* - `ObjectDataTable` and `RelatedList` RESOLVE the legacy spelling, stamping
* `accessorKey` from `columnIdentity(col)` before delivery.
* - `ObjectGrid` does NOT fold. Since objectui#5068 it REFUSES undeclared
* spellings at intake — `resolvesToDataColumn` requires a string `field`
* (`columnSpellingDiagnostics.ts`), so a `name`-spelled entry is dropped
* before delivery and never reaches this adapter at all (before AND after
* this change; that silence is objectui#5352's territory). What it does
* deliver carries `accessorKey` stamped from `field`.
*
* The 2026-08-20 ruling's item 2 told ObjectGrid to "connect to the same
* `columnIdentity` resolution"; objectui#5478 measured that following it would
* have RE-WIDENED what #5068 narrowed. Stated here because the claim has been
* restated wrongly more than once, and a third repetition would settle it.
*
* Either way the adapter never sees a legacy spelling from a producer. What
* stops resolving is `name` on a column authored DIRECTLY onto a `data-table`
* node. That is the whole blast radius, and it is pinned below in both
* directions.
*
* The two aliases were DIFFERENT failure classes, which is why the cards were
* filed apart: an unresolved `accessorKey` gives blank cells under a live
Expand DownExpand Up@@ -79,24 +105,52 @@ describe('data-table columns — the declared keys render (unchanged)', () => {
});
});

describe('data-table columns — the `name` alias is still read, and that is a HOLD (#5120)', () => {
it('still resolves an accessor from the undeclared `name`', () => {
// NOT an endorsement — a receipt. The 2026-08-20 ruling retires this limb;
// what stops it today is that two published skill guides teach it and
// `skill-guide-data-table-binding.test.tsx` renders their bytes. Pinning the
// CURRENT behaviour means the day the guides move, this test goes red and
// names itself as the thing to delete, instead of the retirement quietly
// never happening.
describe('data-table columns — the undeclared `name` alias is retired (#5120)', () => {
it('does not resolve an accessor from `name`', () => {
// The narrowing this card ships, and the receipt that replaces the HOLD pin
// that stood here while the instruction corpus still taught `name`.
// A DIFFERENT failure class from #5351's: the header is fine and the CELLS
// are what go blank.
renderTable([{ header: 'Stage', name: 'stage' }]);
expect(bodyCells()).toEqual(['Won', 'Lost']);
expect(headers()).toEqual(['Stage']);
expect(bodyCells()).toEqual(['', '']);
});

it('keeps an authored `accessorKey` ahead of a divergent `name`', () => {
it('keeps an authored `accessorKey` winning over a divergent `name`', () => {
// Precedence, unchanged and load bearing: columns can arrive in the table
// library's own shape, and those must not be second-guessed.
// library's own shape, and those must not be second-guessed. This passed
// before the retirement too — it is here so the two directions cannot drift.
renderTable([{ header: 'Stage', accessorKey: 'stage', name: 'nonsense' }]);
expect(bodyCells()).toEqual(['Won', 'Lost']);
});

it('LEGIBILITY: an unresolvable column keeps its header and spares its neighbour', () => {
// Measured, not assumed, and deliberately pinned as the SHAPE OF THE BREAK:
// the column is not dropped and nothing throws — a live header sits over
// blank cells while the declared neighbour is untouched. This is the failure
// an author who kept the legacy spelling now gets, and it is the same
// illegible shape objectui#5349 measures against.
renderTable([
{ header: 'Stage', name: 'stage' },
{ header: 'Id', accessorKey: 'id' },
]);
expect(headers()).toEqual(['Stage', 'Id']);
expect(bodyCells()).toEqual(['', '1', '', '2']);
});

it('a PRODUCER-resolved column is unaffected — the narrowing is adapter-only', () => {
// The blast-radius bound in one assertion: whatever a producer delivers
// already carries a declared `accessorKey`, so the retirement cannot reach
// it. ObjectDataTable and RelatedList get there by RESOLVING `name` through
// `columnIdentity`; ObjectGrid gets there by REFUSING undeclared spellings
// at intake and stamping from `field` (see the header — the two routes are
// not the same mechanism). Simulating the hand-off here keeps this file
// honest about what the retirement does and does not break, without
// importing a plugin package.
renderTable([{ header: 'Stage', name: 'stage', accessorKey: 'stage' }]);
expect(headers()).toEqual(['Stage']);
expect(bodyCells()).toEqual(['Won', 'Lost']);
});
});

describe('data-table columns — the undeclared `label` alias is retired (#5351)', () => {
Expand Down
41 changes: 23 additions & 18 deletions packages/components/src/renderers/complex/data-table.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -832,9 +832,9 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
if (nonArrayDataMessage) console.warn(nonArrayDataMessage);
}, [nonArrayDataMessage]);

// The adapter reads the column keys `TableColumn` DECLARES. The `label`
// alias is gone (objectui#5351); the `name` alias is HELD, and the hold is
// deliberate and documented rather than an oversight.
// The adapter reads ONLY the column keys `TableColumn` DECLARES. Both
// undeclared aliases are now retired: `label` (objectui#5351) and `name`
// (objectui#5120, this change).
//
// These two lines used to normalize each column as
// `header: col.header || col.label` and
Expand All@@ -846,28 +846,33 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
// ruling settled the direction for the whole family: retire the consumer-side
// alias, unify the producers.
//
// `header` has retired. Where its translation went — `columnHeader` in
// `@object-ui/core`, called by each producer before delivery:
// Where the translation went — `columnIdentity` / `columnHeader` in
// `@object-ui/core`, called by each producer BEFORE delivery:
// `ObjectDataTable.normalizeColumns` (`@object-ui/plugin-dashboard`)
// `RelatedList.normalizeColumn` (`@object-ui/plugin-detail`)
// `ObjectGrid.generateColumns` (`@object-ui/plugin-grid`, since #5068)
// Metadata vocabulary in, adapter vocabulary out; one translation, one place.
// Metadata vocabulary in, adapter vocabulary out; one translation, one place —
// and that place is each producer, never here. A `{ name, label }` column
// arriving through any of the three still renders: its producer resolved the
// identity into `accessorKey` before the adapter ever saw it. What no longer
// resolves is `name` on a column authored DIRECTLY onto a `data-table` node,
// which is the accepted-set narrowing objectui#5120 rules and ships.
//
// `accessorKey || col.name` STAYS, pending objectui#5120's remaining step. It
// is not that the producers still need it — all three resolve `accessorKey`
// themselves, measured — but that two PUBLISHED skill guides teach a directly
// authored `data-table` whose columns are spelled `{ name, label }`:
// The instruction corpus moved in the SAME commit, which is the whole reason
// this step could be taken: `skill-guide-data-table-binding.test.tsx` lifts the
// fenced JSON out of the published guides at run time and renders it, so the
// guides are executable fixtures rather than prose. Both now teach
// `{ header, accessorKey }`:
// skills/objectui/guides/data-integration.md
// skills/objectui/guides/schema-expressions.md
// `skill-guide-data-table-binding.test.tsx` lifts those blocks out of the real
// files at run time and renders them, so retiring `name` here turns that gate
// red until the guides move. Retiring the runtime ahead of the instruction
// would leave the platform refusing a spelling it still ships, and the failure
// it teaches into is the illegible one below: a header over blank cells.
// Retiring the runtime ahead of the instruction would have left the platform
// refusing a spelling it still shipped, and the failure it teaches into is the
// illegible one: a header over blank cells (pinned in
// `data-table-declared-column-keys.test.tsx`).
const initialColumns = useMemo(() => {
return rawColumns.map((col: any) => ({
...col,
accessorKey: col.accessorKey || col.name,
accessorKey: col.accessorKey,
}));
}, [rawColumns]);

Expand All@@ -876,10 +881,10 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
const widths: Record<string, number> = {};
// Spelled identically to `initialColumns` above — the auto-width pass must
// measure the SAME columns the table renders, so the two reads move
// together (objectui#5351 retired `header`'s alias; `name`'s is held).
// together (objectui#5351 retired `header`'s alias, objectui#5120 `name`'s).
const cols = rawColumns.map((col: any) => ({
header: col.header,
accessorKey: col.accessorKey || col.name,
accessorKey: col.accessorKey,
width: col.width,
fitContent: col.fitContent,
}));
Expand Down
4 changes: 2 additions & 2 deletions skills/objectui/guides/data-integration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,8 +217,8 @@ through is measured, with its open-question caveat, in
{ "name": "Grace Hopper", "email": "grace@example.com" }
],
"columns": [
{ "name": "name", "label": "Name" },
{ "name": "email", "label": "Email" }
{ "header": "Name", "accessorKey": "name" },
{ "header": "Email", "accessorKey": "email" }
]
}
```
Expand Down
4 changes: 2 additions & 2 deletions skills/objectui/guides/schema-expressions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -421,8 +421,8 @@ and is blank is the whole failure.
{ "name": "Grace Hopper", "email": "grace@example.com" }
],
"columns": [
{ "name": "name", "label": "Name" },
{ "name": "email", "label": "Email" }
{ "header": "Name", "accessorKey": "name" },
{ "header": "Email", "accessorKey": "email" }
]
}
```
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .changeset/5120-retire-data-table-name-alias.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
---
'@object-ui/components': minor
---

**Breaking for authored metadata:** a `data-table` column spelled `name` no
longer resolves its cells. Use the declared `accessorKey`.

`data-table`'s column normalization used to read
`accessorKey: col.accessorKey || col.name` — but `TableColumn`
(`@object-ui/types`) declares only `accessorKey`, never `name`. The declared
surface admitted one spelling while the runtime admitted two, which is the
second de-facto contract AGENTS.md #0.1 forbids. The maintainer ruling of
2026-08-20 settled the direction for the whole family: retire the consumer-side
alias, translate at the producers. `label` → `header` retired first
(objectui#5351); this retires `name` → `accessorKey` and closes the family.

**Who is affected — a column authored DIRECTLY onto a `data-table` node:**

```json
{ "type": "data-table",
"columns": [{ "name": "email", "label": "Email" }] } // ← was tolerated
```

becomes

```json
{ "type": "data-table",
"columns": [{ "header": "Email", "accessorKey": "email" }] }
```

**Who is NOT affected.** Columns reaching the table through `object-data-table`,
a detail view's `related[]` list, or `object-grid` are unchanged — the adapter
never sees a legacy spelling from any of them. The reason differs by producer,
and the difference matters if you are debugging one:

- `object-data-table` and a detail view's `related[]` list **resolve** the
legacy spelling before delivery, stamping `accessorKey` from `name` (via
`columnIdentity`). A `name`-spelled column keeps working there.
- `object-grid` **refuses** it instead: since objectui#5068 an authored column
must spell the declared `field`, and one that does not is dropped at intake
and never reaches the table. Its delivered columns carry `accessorKey`
stamped from `field`. So a `name`-spelled `object-grid` column does not
render today either — that is objectui#5352's open question, unchanged by
this release.

Only the directly-authored `data-table` node narrows here.

**How the break presents, so you can recognise it:** the column is not dropped
and nothing is thrown — its header still renders over blank cells, and
neighbouring columns are unaffected. If a table's header row looks right but one
column's cells are empty, check that column's key spelling first.

The two published skill guides that taught the `name` spelling
(`skills/objectui/guides/data-integration.md`, `schema-expressions.md`) migrate
in this same release, so the platform never refuses a spelling it still ships.

Graded `minor`, not `patch`: this narrows the accepted input set, which is a
breaking change for any author who used the tolerated spelling. It is not
`major` per this repo's fixed-group convention (objectui's own breaking changes
ship as `minor`; the group's major tracks `@objectstack`).
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,11 +40,24 @@
* empty state. The correction is verified against the renderer rather
* than against a reading of it.
*
* The `columns` entries in these examples are deliberately untouched: their
* `{ name, label }` spelling is the separate open question on objectui#5120
* (the undeclared `col.name` / `col.label` alias at `data-table.tsx:776-777`),
* parked with the maintainer. This file pins the BINDING only, and stays true
* whichever way that one lands — nothing below asserts a column key spelling.
* ⚠️ COLUMN SPELLING — this file is NOT spelling-independent, and an earlier
* revision of this docblock said it was. It claimed the file "pins the BINDING
* only … nothing below asserts a column key spelling". That was false and was
* filed as objectui#5479: the BEHAVIOUR assertions below are on rendered CELL
* TEXT, and a cell only has text when its column's accessor resolves — so they
* transitively pin the accessor spelling the guides use. A docblock asserting
* independence over assertions that were not independent is what made
* objectui#5120's last step invisible when the family was ruled on.
*
* Both aliases have since retired — `label` in objectui#5351, `name` in
* objectui#5120 — and the two guides' `data-table` columns migrated to the
* declared `{ header, accessorKey }` in the same commit as the `name`
* retirement, because these blocks are lifted and rendered at run time and
* would otherwise go blank. That coupling is the point, not a defect: the
* guides are executable fixtures, so the instruction corpus cannot drift from
* the runtime without this file going red. Keep it that way — if a future
* change makes the adapter's accepted key set narrower again, migrate the
* guides in the SAME commit.
*/

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
Expand DownExpand Up@@ -162,6 +175,31 @@ describe('skill guides — no `data-table` example is bound with `bind` (#5126,
expect(offenders).toEqual([]);
});

it('no `data-table` example spells a column with the retired `name` (#5120)', () => {
// objectui#5120 retired `accessorKey: col.accessorKey || col.name` on the
// adapter. These blocks are RENDERED below, so a guide that regressed to
// `name` would already fail — but only as "expected [] to equal
// ['Ada Lovelace', …]", which names neither the key nor the card. This
// assertion is here to make the failure SAY what broke, and to hold the
// corpus even if the behaviour legs are ever narrowed.
for (const rel of GUIDE_PATHS) {
const md = readGuide(rel);
for (const node of blocksOfType(md, 'data-table')) {
const columns = (node.columns ?? []) as Record<string, unknown>[];
for (const col of columns) {
expect(
'name' in col,
`${rel}: a data-table column spells the retired \`name\`; the declared key is \`accessorKey\` (objectui#5120)`,
).toBe(false);
expect(
'accessorKey' in col,
`${rel}: a data-table column is missing the declared \`accessorKey\` (objectui#5120)`,
).toBe(true);
}
}
}
});

it('no guide claims a table component calls useDataScope', () => {
for (const rel of GUIDE_PATHS) {
const md = readGuide(rel);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,9 +57,16 @@ const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, '../../../..');
const skillsRoot = path.join(repoRoot, 'skills/objectui');

// The DECLARED column keys (`TableColumn`: `header` + `accessorKey`). These
// mirror the two published guides' `data-table` example, which is why they
// moved when the guides did: objectui#5120 retired the adapter's undeclared
// `col.name` alias, so the `{ name, label }` spelling this fixture used to
// carry now resolves no accessor and every cell below would read ''. The rows
// keep `name`/`email` as their own DATA keys — that is what `accessorKey`
// points AT, and it is unrelated to the column vocabulary.
const COLUMNS = [
{ name: 'name', label: 'Name' },
{ name: 'email', label: 'Email' },
{ header: 'Name', accessorKey: 'name' },
{ header: 'Email', accessorKey: 'email' },
];
const ROWS = [
{ name: 'Ada Lovelace', email: 'ada@example.com' },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,8 +7,9 @@
*/

/**
* `data-table` reads the DECLARED `header`, not the undeclared `label`
* (objectui#5351).
* `data-table` reads the DECLARED `header` and `accessorKey`, and NOTHING else
* — not the undeclared `label` (objectui#5351), not the undeclared `name`
* (objectui#5120).
*
* `TableColumn` (`packages/types/src/data-display.ts`) declares `header: string`
* and `accessorKey: string`. It declares neither `label` nor `name`. The
Expand All@@ -26,14 +27,39 @@
* purpose. Metadata vocabulary in, adapter vocabulary out; one translation, one
* place — and that place is each producer, never here.
*
* SCOPE. The `label` alias retires here; the `name` alias is HELD, and the last
* describe below pins it as still-read so the hold cannot be mistaken for the
* retirement having happened. Two published skill guides teach a directly
* authored `data-table` whose columns are spelled `{ name, label }`, and
* SCOPE. `label` retired in objectui#5351; `name` retires here, closing the
* family. The hold that kept `name` alive was never about the producers — all
* three resolve `accessorKey` themselves, measured — but about the INSTRUCTION
* CORPUS: two published skill guides taught a directly authored `data-table`
* whose columns were spelled `{ name, label }`, and
* `skill-guide-data-table-binding.test.tsx` renders those blocks straight out of
* the guide files — so `name` cannot retire until the instruction corpus moves.
* That is objectui#5120's remaining step and is nobody's to take unbidden:
* `skills/**` is a customer-published surface with its own owning seat.
* the guide files. Both guides migrated to `{ header, accessorKey }` in the same
* commit as this retirement, which is the only ordering in which the platform
* never refuses a spelling it still ships.
*
* WHAT NARROWS, precisely. A `{ name, label }` column arriving through
* `ObjectDataTable`, `RelatedList` or `ObjectGrid` is UNAFFECTED — but for two
* different reasons, and conflating them is a mistake this card's own ruling
* already made once:
*
* - `ObjectDataTable` and `RelatedList` RESOLVE the legacy spelling, stamping
* `accessorKey` from `columnIdentity(col)` before delivery.
* - `ObjectGrid` does NOT fold. Since objectui#5068 it REFUSES undeclared
* spellings at intake — `resolvesToDataColumn` requires a string `field`
* (`columnSpellingDiagnostics.ts`), so a `name`-spelled entry is dropped
* before delivery and never reaches this adapter at all (before AND after
* this change; that silence is objectui#5352's territory). What it does
* deliver carries `accessorKey` stamped from `field`.
*
* The 2026-08-20 ruling's item 2 told ObjectGrid to "connect to the same
* `columnIdentity` resolution"; objectui#5478 measured that following it would
* have RE-WIDENED what #5068 narrowed. Stated here because the claim has been
* restated wrongly more than once, and a third repetition would settle it.
*
* Either way the adapter never sees a legacy spelling from a producer. What
* stops resolving is `name` on a column authored DIRECTLY onto a `data-table`
* node. That is the whole blast radius, and it is pinned below in both
* directions.
*
* The two aliases were DIFFERENT failure classes, which is why the cards were
* filed apart: an unresolved `accessorKey` gives blank cells under a live
Expand DownExpand Up@@ -79,24 +105,52 @@ describe('data-table columns — the declared keys render (unchanged)', () => {
});
});

describe('data-table columns — the `name` alias is still read, and that is a HOLD (#5120)', () => {
it('still resolves an accessor from the undeclared `name`', () => {
// NOT an endorsement — a receipt. The 2026-08-20 ruling retires this limb;
// what stops it today is that two published skill guides teach it and
// `skill-guide-data-table-binding.test.tsx` renders their bytes. Pinning the
// CURRENT behaviour means the day the guides move, this test goes red and
// names itself as the thing to delete, instead of the retirement quietly
// never happening.
describe('data-table columns — the undeclared `name` alias is retired (#5120)', () => {
it('does not resolve an accessor from `name`', () => {
// The narrowing this card ships, and the receipt that replaces the HOLD pin
// that stood here while the instruction corpus still taught `name`.
// A DIFFERENT failure class from #5351's: the header is fine and the CELLS
// are what go blank.
renderTable([{ header: 'Stage', name: 'stage' }]);
expect(bodyCells()).toEqual(['Won', 'Lost']);
expect(headers()).toEqual(['Stage']);
expect(bodyCells()).toEqual(['', '']);
});

it('keeps an authored `accessorKey` ahead of a divergent `name`', () => {
it('keeps an authored `accessorKey` winning over a divergent `name`', () => {
// Precedence, unchanged and load bearing: columns can arrive in the table
// library's own shape, and those must not be second-guessed.
// library's own shape, and those must not be second-guessed. This passed
// before the retirement too — it is here so the two directions cannot drift.
renderTable([{ header: 'Stage', accessorKey: 'stage', name: 'nonsense' }]);
expect(bodyCells()).toEqual(['Won', 'Lost']);
});

it('LEGIBILITY: an unresolvable column keeps its header and spares its neighbour', () => {
// Measured, not assumed, and deliberately pinned as the SHAPE OF THE BREAK:
// the column is not dropped and nothing throws — a live header sits over
// blank cells while the declared neighbour is untouched. This is the failure
// an author who kept the legacy spelling now gets, and it is the same
// illegible shape objectui#5349 measures against.
renderTable([
{ header: 'Stage', name: 'stage' },
{ header: 'Id', accessorKey: 'id' },
]);
expect(headers()).toEqual(['Stage', 'Id']);
expect(bodyCells()).toEqual(['', '1', '', '2']);
});

it('a PRODUCER-resolved column is unaffected — the narrowing is adapter-only', () => {
// The blast-radius bound in one assertion: whatever a producer delivers
// already carries a declared `accessorKey`, so the retirement cannot reach
// it. ObjectDataTable and RelatedList get there by RESOLVING `name` through
// `columnIdentity`; ObjectGrid gets there by REFUSING undeclared spellings
// at intake and stamping from `field` (see the header — the two routes are
// not the same mechanism). Simulating the hand-off here keeps this file
// honest about what the retirement does and does not break, without
// importing a plugin package.
renderTable([{ header: 'Stage', name: 'stage', accessorKey: 'stage' }]);
expect(headers()).toEqual(['Stage']);
expect(bodyCells()).toEqual(['Won', 'Lost']);
});
});

describe('data-table columns — the undeclared `label` alias is retired (#5351)', () => {
Expand Down
41 changes: 23 additions & 18 deletions packages/components/src/renderers/complex/data-table.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -832,9 +832,9 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
if (nonArrayDataMessage) console.warn(nonArrayDataMessage);
}, [nonArrayDataMessage]);

// The adapter reads the column keys `TableColumn` DECLARES. The `label`
// alias is gone (objectui#5351); the `name` alias is HELD, and the hold is
// deliberate and documented rather than an oversight.
// The adapter reads ONLY the column keys `TableColumn` DECLARES. Both
// undeclared aliases are now retired: `label` (objectui#5351) and `name`
// (objectui#5120, this change).
//
// These two lines used to normalize each column as
// `header: col.header || col.label` and
Expand All@@ -846,28 +846,33 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
// ruling settled the direction for the whole family: retire the consumer-side
// alias, unify the producers.
//
// `header` has retired. Where its translation went — `columnHeader` in
// `@object-ui/core`, called by each producer before delivery:
// Where the translation went — `columnIdentity` / `columnHeader` in
// `@object-ui/core`, called by each producer BEFORE delivery:
// `ObjectDataTable.normalizeColumns` (`@object-ui/plugin-dashboard`)
// `RelatedList.normalizeColumn` (`@object-ui/plugin-detail`)
// `ObjectGrid.generateColumns` (`@object-ui/plugin-grid`, since #5068)
// Metadata vocabulary in, adapter vocabulary out; one translation, one place.
// Metadata vocabulary in, adapter vocabulary out; one translation, one place —
// and that place is each producer, never here. A `{ name, label }` column
// arriving through any of the three still renders: its producer resolved the
// identity into `accessorKey` before the adapter ever saw it. What no longer
// resolves is `name` on a column authored DIRECTLY onto a `data-table` node,
// which is the accepted-set narrowing objectui#5120 rules and ships.
//
// `accessorKey || col.name` STAYS, pending objectui#5120's remaining step. It
// is not that the producers still need it — all three resolve `accessorKey`
// themselves, measured — but that two PUBLISHED skill guides teach a directly
// authored `data-table` whose columns are spelled `{ name, label }`:
// The instruction corpus moved in the SAME commit, which is the whole reason
// this step could be taken: `skill-guide-data-table-binding.test.tsx` lifts the
// fenced JSON out of the published guides at run time and renders it, so the
// guides are executable fixtures rather than prose. Both now teach
// `{ header, accessorKey }`:
// skills/objectui/guides/data-integration.md
// skills/objectui/guides/schema-expressions.md
// `skill-guide-data-table-binding.test.tsx` lifts those blocks out of the real
// files at run time and renders them, so retiring `name` here turns that gate
// red until the guides move. Retiring the runtime ahead of the instruction
// would leave the platform refusing a spelling it still ships, and the failure
// it teaches into is the illegible one below: a header over blank cells.
// Retiring the runtime ahead of the instruction would have left the platform
// refusing a spelling it still shipped, and the failure it teaches into is the
// illegible one: a header over blank cells (pinned in
// `data-table-declared-column-keys.test.tsx`).
const initialColumns = useMemo(() => {
return rawColumns.map((col: any) => ({
...col,
accessorKey: col.accessorKey || col.name,
accessorKey: col.accessorKey,
}));
}, [rawColumns]);

Expand All@@ -876,10 +881,10 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
const widths: Record<string, number> = {};
// Spelled identically to `initialColumns` above — the auto-width pass must
// measure the SAME columns the table renders, so the two reads move
// together (objectui#5351 retired `header`'s alias; `name`'s is held).
// together (objectui#5351 retired `header`'s alias, objectui#5120 `name`'s).
const cols = rawColumns.map((col: any) => ({
header: col.header,
accessorKey: col.accessorKey || col.name,
accessorKey: col.accessorKey,
width: col.width,
fitContent: col.fitContent,
}));
Expand Down
4 changes: 2 additions & 2 deletions skills/objectui/guides/data-integration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,8 +217,8 @@ through is measured, with its open-question caveat, in
{ "name": "Grace Hopper", "email": "grace@example.com" }
],
"columns": [
{ "name": "name", "label": "Name" },
{ "name": "email", "label": "Email" }
{ "header": "Name", "accessorKey": "name" },
{ "header": "Email", "accessorKey": "email" }
]
}
```
Expand Down
4 changes: 2 additions & 2 deletions skills/objectui/guides/schema-expressions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -421,8 +421,8 @@ and is blank is the whole failure.
{ "name": "Grace Hopper", "email": "grace@example.com" }
],
"columns": [
{ "name": "name", "label": "Name" },
{ "name": "email", "label": "Email" }
{ "header": "Name", "accessorKey": "name" },
{ "header": "Email", "accessorKey": "email" }
]
}
```
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .changeset/5120-retire-data-table-name-alias.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
---
'@object-ui/components': minor
---

**Breaking for authored metadata:** a `data-table` column spelled `name` no
longer resolves its cells. Use the declared `accessorKey`.

`data-table`'s column normalization used to read
`accessorKey: col.accessorKey || col.name` — but `TableColumn`
(`@object-ui/types`) declares only `accessorKey`, never `name`. The declared
surface admitted one spelling while the runtime admitted two, which is the
second de-facto contract AGENTS.md #0.1 forbids. The maintainer ruling of
2026-08-20 settled the direction for the whole family: retire the consumer-side
alias, translate at the producers. `label` → `header` retired first
(objectui#5351); this retires `name` → `accessorKey` and closes the family.

**Who is affected — a column authored DIRECTLY onto a `data-table` node:**

```json
{ "type": "data-table",
"columns": [{ "name": "email", "label": "Email" }] } // ← was tolerated
```

becomes

```json
{ "type": "data-table",
"columns": [{ "header": "Email", "accessorKey": "email" }] }
```

**Who is NOT affected.** Columns reaching the table through `object-data-table`,
a detail view's `related[]` list, or `object-grid` are unchanged — the adapter
never sees a legacy spelling from any of them. The reason differs by producer,
and the difference matters if you are debugging one:

- `object-data-table` and a detail view's `related[]` list **resolve** the
legacy spelling before delivery, stamping `accessorKey` from `name` (via
`columnIdentity`). A `name`-spelled column keeps working there.
- `object-grid` **refuses** it instead: since objectui#5068 an authored column
must spell the declared `field`, and one that does not is dropped at intake
and never reaches the table. Its delivered columns carry `accessorKey`
stamped from `field`. So a `name`-spelled `object-grid` column does not
render today either — that is objectui#5352's open question, unchanged by
this release.

Only the directly-authored `data-table` node narrows here.

**How the break presents, so you can recognise it:** the column is not dropped
and nothing is thrown — its header still renders over blank cells, and
neighbouring columns are unaffected. If a table's header row looks right but one
column's cells are empty, check that column's key spelling first.

The two published skill guides that taught the `name` spelling
(`skills/objectui/guides/data-integration.md`, `schema-expressions.md`) migrate
in this same release, so the platform never refuses a spelling it still ships.

Graded `minor`, not `patch`: this narrows the accepted input set, which is a
breaking change for any author who used the tolerated spelling. It is not
`major` per this repo's fixed-group convention (objectui's own breaking changes
ship as `minor`; the group's major tracks `@objectstack`).
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,11 +40,24 @@
* empty state. The correction is verified against the renderer rather
* than against a reading of it.
*
* The `columns` entries in these examples are deliberately untouched: their
* `{ name, label }` spelling is the separate open question on objectui#5120
* (the undeclared `col.name` / `col.label` alias at `data-table.tsx:776-777`),
* parked with the maintainer. This file pins the BINDING only, and stays true
* whichever way that one lands — nothing below asserts a column key spelling.
* ⚠️ COLUMN SPELLING — this file is NOT spelling-independent, and an earlier
* revision of this docblock said it was. It claimed the file "pins the BINDING
* only … nothing below asserts a column key spelling". That was false and was
* filed as objectui#5479: the BEHAVIOUR assertions below are on rendered CELL
* TEXT, and a cell only has text when its column's accessor resolves — so they
* transitively pin the accessor spelling the guides use. A docblock asserting
* independence over assertions that were not independent is what made
* objectui#5120's last step invisible when the family was ruled on.
*
* Both aliases have since retired — `label` in objectui#5351, `name` in
* objectui#5120 — and the two guides' `data-table` columns migrated to the
* declared `{ header, accessorKey }` in the same commit as the `name`
* retirement, because these blocks are lifted and rendered at run time and
* would otherwise go blank. That coupling is the point, not a defect: the
* guides are executable fixtures, so the instruction corpus cannot drift from
* the runtime without this file going red. Keep it that way — if a future
* change makes the adapter's accepted key set narrower again, migrate the
* guides in the SAME commit.
*/

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
Expand DownExpand Up@@ -162,6 +175,31 @@ describe('skill guides — no `data-table` example is bound with `bind` (#5126,
expect(offenders).toEqual([]);
});

it('no `data-table` example spells a column with the retired `name` (#5120)', () => {
// objectui#5120 retired `accessorKey: col.accessorKey || col.name` on the
// adapter. These blocks are RENDERED below, so a guide that regressed to
// `name` would already fail — but only as "expected [] to equal
// ['Ada Lovelace', …]", which names neither the key nor the card. This
// assertion is here to make the failure SAY what broke, and to hold the
// corpus even if the behaviour legs are ever narrowed.
for (const rel of GUIDE_PATHS) {
const md = readGuide(rel);
for (const node of blocksOfType(md, 'data-table')) {
const columns = (node.columns ?? []) as Record<string, unknown>[];
for (const col of columns) {
expect(
'name' in col,
`${rel}: a data-table column spells the retired \`name\`; the declared key is \`accessorKey\` (objectui#5120)`,
).toBe(false);
expect(
'accessorKey' in col,
`${rel}: a data-table column is missing the declared \`accessorKey\` (objectui#5120)`,
).toBe(true);
}
}
}
});

it('no guide claims a table component calls useDataScope', () => {
for (const rel of GUIDE_PATHS) {
const md = readGuide(rel);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,9 +57,16 @@ const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, '../../../..');
const skillsRoot = path.join(repoRoot, 'skills/objectui');

// The DECLARED column keys (`TableColumn`: `header` + `accessorKey`). These
// mirror the two published guides' `data-table` example, which is why they
// moved when the guides did: objectui#5120 retired the adapter's undeclared
// `col.name` alias, so the `{ name, label }` spelling this fixture used to
// carry now resolves no accessor and every cell below would read ''. The rows
// keep `name`/`email` as their own DATA keys — that is what `accessorKey`
// points AT, and it is unrelated to the column vocabulary.
const COLUMNS = [
{ name: 'name', label: 'Name' },
{ name: 'email', label: 'Email' },
{ header: 'Name', accessorKey: 'name' },
{ header: 'Email', accessorKey: 'email' },
];
const ROWS = [
{ name: 'Ada Lovelace', email: 'ada@example.com' },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,8 +7,9 @@
*/

/**
* `data-table` reads the DECLARED `header`, not the undeclared `label`
* (objectui#5351).
* `data-table` reads the DECLARED `header` and `accessorKey`, and NOTHING else
* — not the undeclared `label` (objectui#5351), not the undeclared `name`
* (objectui#5120).
*
* `TableColumn` (`packages/types/src/data-display.ts`) declares `header: string`
* and `accessorKey: string`. It declares neither `label` nor `name`. The
Expand All@@ -26,14 +27,39 @@
* purpose. Metadata vocabulary in, adapter vocabulary out; one translation, one
* place — and that place is each producer, never here.
*
* SCOPE. The `label` alias retires here; the `name` alias is HELD, and the last
* describe below pins it as still-read so the hold cannot be mistaken for the
* retirement having happened. Two published skill guides teach a directly
* authored `data-table` whose columns are spelled `{ name, label }`, and
* SCOPE. `label` retired in objectui#5351; `name` retires here, closing the
* family. The hold that kept `name` alive was never about the producers — all
* three resolve `accessorKey` themselves, measured — but about the INSTRUCTION
* CORPUS: two published skill guides taught a directly authored `data-table`
* whose columns were spelled `{ name, label }`, and
* `skill-guide-data-table-binding.test.tsx` renders those blocks straight out of
* the guide files — so `name` cannot retire until the instruction corpus moves.
* That is objectui#5120's remaining step and is nobody's to take unbidden:
* `skills/**` is a customer-published surface with its own owning seat.
* the guide files. Both guides migrated to `{ header, accessorKey }` in the same
* commit as this retirement, which is the only ordering in which the platform
* never refuses a spelling it still ships.
*
* WHAT NARROWS, precisely. A `{ name, label }` column arriving through
* `ObjectDataTable`, `RelatedList` or `ObjectGrid` is UNAFFECTED — but for two
* different reasons, and conflating them is a mistake this card's own ruling
* already made once:
*
* - `ObjectDataTable` and `RelatedList` RESOLVE the legacy spelling, stamping
* `accessorKey` from `columnIdentity(col)` before delivery.
* - `ObjectGrid` does NOT fold. Since objectui#5068 it REFUSES undeclared
* spellings at intake — `resolvesToDataColumn` requires a string `field`
* (`columnSpellingDiagnostics.ts`), so a `name`-spelled entry is dropped
* before delivery and never reaches this adapter at all (before AND after
* this change; that silence is objectui#5352's territory). What it does
* deliver carries `accessorKey` stamped from `field`.
*
* The 2026-08-20 ruling's item 2 told ObjectGrid to "connect to the same
* `columnIdentity` resolution"; objectui#5478 measured that following it would
* have RE-WIDENED what #5068 narrowed. Stated here because the claim has been
* restated wrongly more than once, and a third repetition would settle it.
*
* Either way the adapter never sees a legacy spelling from a producer. What
* stops resolving is `name` on a column authored DIRECTLY onto a `data-table`
* node. That is the whole blast radius, and it is pinned below in both
* directions.
*
* The two aliases were DIFFERENT failure classes, which is why the cards were
* filed apart: an unresolved `accessorKey` gives blank cells under a live
Expand DownExpand Up@@ -79,24 +105,52 @@ describe('data-table columns — the declared keys render (unchanged)', () => {
});
});

describe('data-table columns — the `name` alias is still read, and that is a HOLD (#5120)', () => {
it('still resolves an accessor from the undeclared `name`', () => {
// NOT an endorsement — a receipt. The 2026-08-20 ruling retires this limb;
// what stops it today is that two published skill guides teach it and
// `skill-guide-data-table-binding.test.tsx` renders their bytes. Pinning the
// CURRENT behaviour means the day the guides move, this test goes red and
// names itself as the thing to delete, instead of the retirement quietly
// never happening.
describe('data-table columns — the undeclared `name` alias is retired (#5120)', () => {
it('does not resolve an accessor from `name`', () => {
// The narrowing this card ships, and the receipt that replaces the HOLD pin
// that stood here while the instruction corpus still taught `name`.
// A DIFFERENT failure class from #5351's: the header is fine and the CELLS
// are what go blank.
renderTable([{ header: 'Stage', name: 'stage' }]);
expect(bodyCells()).toEqual(['Won', 'Lost']);
expect(headers()).toEqual(['Stage']);
expect(bodyCells()).toEqual(['', '']);
});

it('keeps an authored `accessorKey` ahead of a divergent `name`', () => {
it('keeps an authored `accessorKey` winning over a divergent `name`', () => {
// Precedence, unchanged and load bearing: columns can arrive in the table
// library's own shape, and those must not be second-guessed.
// library's own shape, and those must not be second-guessed. This passed
// before the retirement too — it is here so the two directions cannot drift.
renderTable([{ header: 'Stage', accessorKey: 'stage', name: 'nonsense' }]);
expect(bodyCells()).toEqual(['Won', 'Lost']);
});

it('LEGIBILITY: an unresolvable column keeps its header and spares its neighbour', () => {
// Measured, not assumed, and deliberately pinned as the SHAPE OF THE BREAK:
// the column is not dropped and nothing throws — a live header sits over
// blank cells while the declared neighbour is untouched. This is the failure
// an author who kept the legacy spelling now gets, and it is the same
// illegible shape objectui#5349 measures against.
renderTable([
{ header: 'Stage', name: 'stage' },
{ header: 'Id', accessorKey: 'id' },
]);
expect(headers()).toEqual(['Stage', 'Id']);
expect(bodyCells()).toEqual(['', '1', '', '2']);
});

it('a PRODUCER-resolved column is unaffected — the narrowing is adapter-only', () => {
// The blast-radius bound in one assertion: whatever a producer delivers
// already carries a declared `accessorKey`, so the retirement cannot reach
// it. ObjectDataTable and RelatedList get there by RESOLVING `name` through
// `columnIdentity`; ObjectGrid gets there by REFUSING undeclared spellings
// at intake and stamping from `field` (see the header — the two routes are
// not the same mechanism). Simulating the hand-off here keeps this file
// honest about what the retirement does and does not break, without
// importing a plugin package.
renderTable([{ header: 'Stage', name: 'stage', accessorKey: 'stage' }]);
expect(headers()).toEqual(['Stage']);
expect(bodyCells()).toEqual(['Won', 'Lost']);
});
});

describe('data-table columns — the undeclared `label` alias is retired (#5351)', () => {
Expand Down
41 changes: 23 additions & 18 deletions packages/components/src/renderers/complex/data-table.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -832,9 +832,9 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
if (nonArrayDataMessage) console.warn(nonArrayDataMessage);
}, [nonArrayDataMessage]);

// The adapter reads the column keys `TableColumn` DECLARES. The `label`
// alias is gone (objectui#5351); the `name` alias is HELD, and the hold is
// deliberate and documented rather than an oversight.
// The adapter reads ONLY the column keys `TableColumn` DECLARES. Both
// undeclared aliases are now retired: `label` (objectui#5351) and `name`
// (objectui#5120, this change).
//
// These two lines used to normalize each column as
// `header: col.header || col.label` and
Expand All@@ -846,28 +846,33 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
// ruling settled the direction for the whole family: retire the consumer-side
// alias, unify the producers.
//
// `header` has retired. Where its translation went — `columnHeader` in
// `@object-ui/core`, called by each producer before delivery:
// Where the translation went — `columnIdentity` / `columnHeader` in
// `@object-ui/core`, called by each producer BEFORE delivery:
// `ObjectDataTable.normalizeColumns` (`@object-ui/plugin-dashboard`)
// `RelatedList.normalizeColumn` (`@object-ui/plugin-detail`)
// `ObjectGrid.generateColumns` (`@object-ui/plugin-grid`, since #5068)
// Metadata vocabulary in, adapter vocabulary out; one translation, one place.
// Metadata vocabulary in, adapter vocabulary out; one translation, one place —
// and that place is each producer, never here. A `{ name, label }` column
// arriving through any of the three still renders: its producer resolved the
// identity into `accessorKey` before the adapter ever saw it. What no longer
// resolves is `name` on a column authored DIRECTLY onto a `data-table` node,
// which is the accepted-set narrowing objectui#5120 rules and ships.
//
// `accessorKey || col.name` STAYS, pending objectui#5120's remaining step. It
// is not that the producers still need it — all three resolve `accessorKey`
// themselves, measured — but that two PUBLISHED skill guides teach a directly
// authored `data-table` whose columns are spelled `{ name, label }`:
// The instruction corpus moved in the SAME commit, which is the whole reason
// this step could be taken: `skill-guide-data-table-binding.test.tsx` lifts the
// fenced JSON out of the published guides at run time and renders it, so the
// guides are executable fixtures rather than prose. Both now teach
// `{ header, accessorKey }`:
// skills/objectui/guides/data-integration.md
// skills/objectui/guides/schema-expressions.md
// `skill-guide-data-table-binding.test.tsx` lifts those blocks out of the real
// files at run time and renders them, so retiring `name` here turns that gate
// red until the guides move. Retiring the runtime ahead of the instruction
// would leave the platform refusing a spelling it still ships, and the failure
// it teaches into is the illegible one below: a header over blank cells.
// Retiring the runtime ahead of the instruction would have left the platform
// refusing a spelling it still shipped, and the failure it teaches into is the
// illegible one: a header over blank cells (pinned in
// `data-table-declared-column-keys.test.tsx`).
const initialColumns = useMemo(() => {
return rawColumns.map((col: any) => ({
...col,
accessorKey: col.accessorKey || col.name,
accessorKey: col.accessorKey,
}));
}, [rawColumns]);

Expand All@@ -876,10 +881,10 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
const widths: Record<string, number> = {};
// Spelled identically to `initialColumns` above — the auto-width pass must
// measure the SAME columns the table renders, so the two reads move
// together (objectui#5351 retired `header`'s alias; `name`'s is held).
// together (objectui#5351 retired `header`'s alias, objectui#5120 `name`'s).
const cols = rawColumns.map((col: any) => ({
header: col.header,
accessorKey: col.accessorKey || col.name,
accessorKey: col.accessorKey,
width: col.width,
fitContent: col.fitContent,
}));
Expand Down
4 changes: 2 additions & 2 deletions skills/objectui/guides/data-integration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,8 +217,8 @@ through is measured, with its open-question caveat, in
{ "name": "Grace Hopper", "email": "grace@example.com" }
],
"columns": [
{ "name": "name", "label": "Name" },
{ "name": "email", "label": "Email" }
{ "header": "Name", "accessorKey": "name" },
{ "header": "Email", "accessorKey": "email" }
]
}
```
Expand Down
4 changes: 2 additions & 2 deletions skills/objectui/guides/schema-expressions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -421,8 +421,8 @@ and is blank is the whole failure.
{ "name": "Grace Hopper", "email": "grace@example.com" }
],
"columns": [
{ "name": "name", "label": "Name" },
{ "name": "email", "label": "Email" }
{ "header": "Name", "accessorKey": "name" },
{ "header": "Email", "accessorKey": "email" }
]
}
```
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .changeset/5120-retire-data-table-name-alias.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
---
'@object-ui/components': minor
---

**Breaking for authored metadata:** a `data-table` column spelled `name` no
longer resolves its cells. Use the declared `accessorKey`.

`data-table`'s column normalization used to read
`accessorKey: col.accessorKey || col.name` — but `TableColumn`
(`@object-ui/types`) declares only `accessorKey`, never `name`. The declared
surface admitted one spelling while the runtime admitted two, which is the
second de-facto contract AGENTS.md #0.1 forbids. The maintainer ruling of
2026-08-20 settled the direction for the whole family: retire the consumer-side
alias, translate at the producers. `label` → `header` retired first
(objectui#5351); this retires `name` → `accessorKey` and closes the family.

**Who is affected — a column authored DIRECTLY onto a `data-table` node:**

```json
{ "type": "data-table",
"columns": [{ "name": "email", "label": "Email" }] } // ← was tolerated
```

becomes

```json
{ "type": "data-table",
"columns": [{ "header": "Email", "accessorKey": "email" }] }
```

**Who is NOT affected.** Columns reaching the table through `object-data-table`,
a detail view's `related[]` list, or `object-grid` are unchanged — the adapter
never sees a legacy spelling from any of them. The reason differs by producer,
and the difference matters if you are debugging one:

- `object-data-table` and a detail view's `related[]` list **resolve** the
legacy spelling before delivery, stamping `accessorKey` from `name` (via
`columnIdentity`). A `name`-spelled column keeps working there.
- `object-grid` **refuses** it instead: since objectui#5068 an authored column
must spell the declared `field`, and one that does not is dropped at intake
and never reaches the table. Its delivered columns carry `accessorKey`
stamped from `field`. So a `name`-spelled `object-grid` column does not
render today either — that is objectui#5352's open question, unchanged by
this release.

Only the directly-authored `data-table` node narrows here.

**How the break presents, so you can recognise it:** the column is not dropped
and nothing is thrown — its header still renders over blank cells, and
neighbouring columns are unaffected. If a table's header row looks right but one
column's cells are empty, check that column's key spelling first.

The two published skill guides that taught the `name` spelling
(`skills/objectui/guides/data-integration.md`, `schema-expressions.md`) migrate
in this same release, so the platform never refuses a spelling it still ships.

Graded `minor`, not `patch`: this narrows the accepted input set, which is a
breaking change for any author who used the tolerated spelling. It is not
`major` per this repo's fixed-group convention (objectui's own breaking changes
ship as `minor`; the group's major tracks `@objectstack`).
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,11 +40,24 @@
* empty state. The correction is verified against the renderer rather
* than against a reading of it.
*
* The `columns` entries in these examples are deliberately untouched: their
* `{ name, label }` spelling is the separate open question on objectui#5120
* (the undeclared `col.name` / `col.label` alias at `data-table.tsx:776-777`),
* parked with the maintainer. This file pins the BINDING only, and stays true
* whichever way that one lands — nothing below asserts a column key spelling.
* ⚠️ COLUMN SPELLING — this file is NOT spelling-independent, and an earlier
* revision of this docblock said it was. It claimed the file "pins the BINDING
* only … nothing below asserts a column key spelling". That was false and was
* filed as objectui#5479: the BEHAVIOUR assertions below are on rendered CELL
* TEXT, and a cell only has text when its column's accessor resolves — so they
* transitively pin the accessor spelling the guides use. A docblock asserting
* independence over assertions that were not independent is what made
* objectui#5120's last step invisible when the family was ruled on.
*
* Both aliases have since retired — `label` in objectui#5351, `name` in
* objectui#5120 — and the two guides' `data-table` columns migrated to the
* declared `{ header, accessorKey }` in the same commit as the `name`
* retirement, because these blocks are lifted and rendered at run time and
* would otherwise go blank. That coupling is the point, not a defect: the
* guides are executable fixtures, so the instruction corpus cannot drift from
* the runtime without this file going red. Keep it that way — if a future
* change makes the adapter's accepted key set narrower again, migrate the
* guides in the SAME commit.
*/

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
Expand DownExpand Up@@ -162,6 +175,31 @@ describe('skill guides — no `data-table` example is bound with `bind` (#5126,
expect(offenders).toEqual([]);
});

it('no `data-table` example spells a column with the retired `name` (#5120)', () => {
// objectui#5120 retired `accessorKey: col.accessorKey || col.name` on the
// adapter. These blocks are RENDERED below, so a guide that regressed to
// `name` would already fail — but only as "expected [] to equal
// ['Ada Lovelace', …]", which names neither the key nor the card. This
// assertion is here to make the failure SAY what broke, and to hold the
// corpus even if the behaviour legs are ever narrowed.
for (const rel of GUIDE_PATHS) {
const md = readGuide(rel);
for (const node of blocksOfType(md, 'data-table')) {
const columns = (node.columns ?? []) as Record<string, unknown>[];
for (const col of columns) {
expect(
'name' in col,
`${rel}: a data-table column spells the retired \`name\`; the declared key is \`accessorKey\` (objectui#5120)`,
).toBe(false);
expect(
'accessorKey' in col,
`${rel}: a data-table column is missing the declared \`accessorKey\` (objectui#5120)`,
).toBe(true);
}
}
}
});

it('no guide claims a table component calls useDataScope', () => {
for (const rel of GUIDE_PATHS) {
const md = readGuide(rel);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,9 +57,16 @@ const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, '../../../..');
const skillsRoot = path.join(repoRoot, 'skills/objectui');

// The DECLARED column keys (`TableColumn`: `header` + `accessorKey`). These
// mirror the two published guides' `data-table` example, which is why they
// moved when the guides did: objectui#5120 retired the adapter's undeclared
// `col.name` alias, so the `{ name, label }` spelling this fixture used to
// carry now resolves no accessor and every cell below would read ''. The rows
// keep `name`/`email` as their own DATA keys — that is what `accessorKey`
// points AT, and it is unrelated to the column vocabulary.
const COLUMNS = [
{ name: 'name', label: 'Name' },
{ name: 'email', label: 'Email' },
{ header: 'Name', accessorKey: 'name' },
{ header: 'Email', accessorKey: 'email' },
];
const ROWS = [
{ name: 'Ada Lovelace', email: 'ada@example.com' },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,8 +7,9 @@
*/

/**
* `data-table` reads the DECLARED `header`, not the undeclared `label`
* (objectui#5351).
* `data-table` reads the DECLARED `header` and `accessorKey`, and NOTHING else
* — not the undeclared `label` (objectui#5351), not the undeclared `name`
* (objectui#5120).
*
* `TableColumn` (`packages/types/src/data-display.ts`) declares `header: string`
* and `accessorKey: string`. It declares neither `label` nor `name`. The
Expand All@@ -26,14 +27,39 @@
* purpose. Metadata vocabulary in, adapter vocabulary out; one translation, one
* place — and that place is each producer, never here.
*
* SCOPE. The `label` alias retires here; the `name` alias is HELD, and the last
* describe below pins it as still-read so the hold cannot be mistaken for the
* retirement having happened. Two published skill guides teach a directly
* authored `data-table` whose columns are spelled `{ name, label }`, and
* SCOPE. `label` retired in objectui#5351; `name` retires here, closing the
* family. The hold that kept `name` alive was never about the producers — all
* three resolve `accessorKey` themselves, measured — but about the INSTRUCTION
* CORPUS: two published skill guides taught a directly authored `data-table`
* whose columns were spelled `{ name, label }`, and
* `skill-guide-data-table-binding.test.tsx` renders those blocks straight out of
* the guide files — so `name` cannot retire until the instruction corpus moves.
* That is objectui#5120's remaining step and is nobody's to take unbidden:
* `skills/**` is a customer-published surface with its own owning seat.
* the guide files. Both guides migrated to `{ header, accessorKey }` in the same
* commit as this retirement, which is the only ordering in which the platform
* never refuses a spelling it still ships.
*
* WHAT NARROWS, precisely. A `{ name, label }` column arriving through
* `ObjectDataTable`, `RelatedList` or `ObjectGrid` is UNAFFECTED — but for two
* different reasons, and conflating them is a mistake this card's own ruling
* already made once:
*
* - `ObjectDataTable` and `RelatedList` RESOLVE the legacy spelling, stamping
* `accessorKey` from `columnIdentity(col)` before delivery.
* - `ObjectGrid` does NOT fold. Since objectui#5068 it REFUSES undeclared
* spellings at intake — `resolvesToDataColumn` requires a string `field`
* (`columnSpellingDiagnostics.ts`), so a `name`-spelled entry is dropped
* before delivery and never reaches this adapter at all (before AND after
* this change; that silence is objectui#5352's territory). What it does
* deliver carries `accessorKey` stamped from `field`.
*
* The 2026-08-20 ruling's item 2 told ObjectGrid to "connect to the same
* `columnIdentity` resolution"; objectui#5478 measured that following it would
* have RE-WIDENED what #5068 narrowed. Stated here because the claim has been
* restated wrongly more than once, and a third repetition would settle it.
*
* Either way the adapter never sees a legacy spelling from a producer. What
* stops resolving is `name` on a column authored DIRECTLY onto a `data-table`
* node. That is the whole blast radius, and it is pinned below in both
* directions.
*
* The two aliases were DIFFERENT failure classes, which is why the cards were
* filed apart: an unresolved `accessorKey` gives blank cells under a live
Expand DownExpand Up@@ -79,24 +105,52 @@ describe('data-table columns — the declared keys render (unchanged)', () => {
});
});

describe('data-table columns — the `name` alias is still read, and that is a HOLD (#5120)', () => {
it('still resolves an accessor from the undeclared `name`', () => {
// NOT an endorsement — a receipt. The 2026-08-20 ruling retires this limb;
// what stops it today is that two published skill guides teach it and
// `skill-guide-data-table-binding.test.tsx` renders their bytes. Pinning the
// CURRENT behaviour means the day the guides move, this test goes red and
// names itself as the thing to delete, instead of the retirement quietly
// never happening.
describe('data-table columns — the undeclared `name` alias is retired (#5120)', () => {
it('does not resolve an accessor from `name`', () => {
// The narrowing this card ships, and the receipt that replaces the HOLD pin
// that stood here while the instruction corpus still taught `name`.
// A DIFFERENT failure class from #5351's: the header is fine and the CELLS
// are what go blank.
renderTable([{ header: 'Stage', name: 'stage' }]);
expect(bodyCells()).toEqual(['Won', 'Lost']);
expect(headers()).toEqual(['Stage']);
expect(bodyCells()).toEqual(['', '']);
});

it('keeps an authored `accessorKey` ahead of a divergent `name`', () => {
it('keeps an authored `accessorKey` winning over a divergent `name`', () => {
// Precedence, unchanged and load bearing: columns can arrive in the table
// library's own shape, and those must not be second-guessed.
// library's own shape, and those must not be second-guessed. This passed
// before the retirement too — it is here so the two directions cannot drift.
renderTable([{ header: 'Stage', accessorKey: 'stage', name: 'nonsense' }]);
expect(bodyCells()).toEqual(['Won', 'Lost']);
});

it('LEGIBILITY: an unresolvable column keeps its header and spares its neighbour', () => {
// Measured, not assumed, and deliberately pinned as the SHAPE OF THE BREAK:
// the column is not dropped and nothing throws — a live header sits over
// blank cells while the declared neighbour is untouched. This is the failure
// an author who kept the legacy spelling now gets, and it is the same
// illegible shape objectui#5349 measures against.
renderTable([
{ header: 'Stage', name: 'stage' },
{ header: 'Id', accessorKey: 'id' },
]);
expect(headers()).toEqual(['Stage', 'Id']);
expect(bodyCells()).toEqual(['', '1', '', '2']);
});

it('a PRODUCER-resolved column is unaffected — the narrowing is adapter-only', () => {
// The blast-radius bound in one assertion: whatever a producer delivers
// already carries a declared `accessorKey`, so the retirement cannot reach
// it. ObjectDataTable and RelatedList get there by RESOLVING `name` through
// `columnIdentity`; ObjectGrid gets there by REFUSING undeclared spellings
// at intake and stamping from `field` (see the header — the two routes are
// not the same mechanism). Simulating the hand-off here keeps this file
// honest about what the retirement does and does not break, without
// importing a plugin package.
renderTable([{ header: 'Stage', name: 'stage', accessorKey: 'stage' }]);
expect(headers()).toEqual(['Stage']);
expect(bodyCells()).toEqual(['Won', 'Lost']);
});
});

describe('data-table columns — the undeclared `label` alias is retired (#5351)', () => {
Expand Down
41 changes: 23 additions & 18 deletions packages/components/src/renderers/complex/data-table.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -832,9 +832,9 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
if (nonArrayDataMessage) console.warn(nonArrayDataMessage);
}, [nonArrayDataMessage]);

// The adapter reads the column keys `TableColumn` DECLARES. The `label`
// alias is gone (objectui#5351); the `name` alias is HELD, and the hold is
// deliberate and documented rather than an oversight.
// The adapter reads ONLY the column keys `TableColumn` DECLARES. Both
// undeclared aliases are now retired: `label` (objectui#5351) and `name`
// (objectui#5120, this change).
//
// These two lines used to normalize each column as
// `header: col.header || col.label` and
Expand All@@ -846,28 +846,33 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
// ruling settled the direction for the whole family: retire the consumer-side
// alias, unify the producers.
//
// `header` has retired. Where its translation went — `columnHeader` in
// `@object-ui/core`, called by each producer before delivery:
// Where the translation went — `columnIdentity` / `columnHeader` in
// `@object-ui/core`, called by each producer BEFORE delivery:
// `ObjectDataTable.normalizeColumns` (`@object-ui/plugin-dashboard`)
// `RelatedList.normalizeColumn` (`@object-ui/plugin-detail`)
// `ObjectGrid.generateColumns` (`@object-ui/plugin-grid`, since #5068)
// Metadata vocabulary in, adapter vocabulary out; one translation, one place.
// Metadata vocabulary in, adapter vocabulary out; one translation, one place —
// and that place is each producer, never here. A `{ name, label }` column
// arriving through any of the three still renders: its producer resolved the
// identity into `accessorKey` before the adapter ever saw it. What no longer
// resolves is `name` on a column authored DIRECTLY onto a `data-table` node,
// which is the accepted-set narrowing objectui#5120 rules and ships.
//
// `accessorKey || col.name` STAYS, pending objectui#5120's remaining step. It
// is not that the producers still need it — all three resolve `accessorKey`
// themselves, measured — but that two PUBLISHED skill guides teach a directly
// authored `data-table` whose columns are spelled `{ name, label }`:
// The instruction corpus moved in the SAME commit, which is the whole reason
// this step could be taken: `skill-guide-data-table-binding.test.tsx` lifts the
// fenced JSON out of the published guides at run time and renders it, so the
// guides are executable fixtures rather than prose. Both now teach
// `{ header, accessorKey }`:
// skills/objectui/guides/data-integration.md
// skills/objectui/guides/schema-expressions.md
// `skill-guide-data-table-binding.test.tsx` lifts those blocks out of the real
// files at run time and renders them, so retiring `name` here turns that gate
// red until the guides move. Retiring the runtime ahead of the instruction
// would leave the platform refusing a spelling it still ships, and the failure
// it teaches into is the illegible one below: a header over blank cells.
// Retiring the runtime ahead of the instruction would have left the platform
// refusing a spelling it still shipped, and the failure it teaches into is the
// illegible one: a header over blank cells (pinned in
// `data-table-declared-column-keys.test.tsx`).
const initialColumns = useMemo(() => {
return rawColumns.map((col: any) => ({
...col,
accessorKey: col.accessorKey || col.name,
accessorKey: col.accessorKey,
}));
}, [rawColumns]);

Expand All@@ -876,10 +881,10 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
const widths: Record<string, number> = {};
// Spelled identically to `initialColumns` above — the auto-width pass must
// measure the SAME columns the table renders, so the two reads move
// together (objectui#5351 retired `header`'s alias; `name`'s is held).
// together (objectui#5351 retired `header`'s alias, objectui#5120 `name`'s).
const cols = rawColumns.map((col: any) => ({
header: col.header,
accessorKey: col.accessorKey || col.name,
accessorKey: col.accessorKey,
width: col.width,
fitContent: col.fitContent,
}));
Expand Down
4 changes: 2 additions & 2 deletions skills/objectui/guides/data-integration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,8 +217,8 @@ through is measured, with its open-question caveat, in
{ "name": "Grace Hopper", "email": "grace@example.com" }
],
"columns": [
{ "name": "name", "label": "Name" },
{ "name": "email", "label": "Email" }
{ "header": "Name", "accessorKey": "name" },
{ "header": "Email", "accessorKey": "email" }
]
}
```
Expand Down
4 changes: 2 additions & 2 deletions skills/objectui/guides/schema-expressions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -421,8 +421,8 @@ and is blank is the whole failure.
{ "name": "Grace Hopper", "email": "grace@example.com" }
],
"columns": [
{ "name": "name", "label": "Name" },
{ "name": "email", "label": "Email" }
{ "header": "Name", "accessorKey": "name" },
{ "header": "Email", "accessorKey": "email" }
]
}
```
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .changeset/5120-retire-data-table-name-alias.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
---
'@object-ui/components': minor
---

**Breaking for authored metadata:** a `data-table` column spelled `name` no
longer resolves its cells. Use the declared `accessorKey`.

`data-table`'s column normalization used to read
`accessorKey: col.accessorKey || col.name` — but `TableColumn`
(`@object-ui/types`) declares only `accessorKey`, never `name`. The declared
surface admitted one spelling while the runtime admitted two, which is the
second de-facto contract AGENTS.md #0.1 forbids. The maintainer ruling of
2026-08-20 settled the direction for the whole family: retire the consumer-side
alias, translate at the producers. `label` → `header` retired first
(objectui#5351); this retires `name` → `accessorKey` and closes the family.

**Who is affected — a column authored DIRECTLY onto a `data-table` node:**

```json
{ "type": "data-table",
"columns": [{ "name": "email", "label": "Email" }] } // ← was tolerated
```

becomes

```json
{ "type": "data-table",
"columns": [{ "header": "Email", "accessorKey": "email" }] }
```

**Who is NOT affected.** Columns reaching the table through `object-data-table`,
a detail view's `related[]` list, or `object-grid` are unchanged — the adapter
never sees a legacy spelling from any of them. The reason differs by producer,
and the difference matters if you are debugging one:

- `object-data-table` and a detail view's `related[]` list **resolve** the
legacy spelling before delivery, stamping `accessorKey` from `name` (via
`columnIdentity`). A `name`-spelled column keeps working there.
- `object-grid` **refuses** it instead: since objectui#5068 an authored column
must spell the declared `field`, and one that does not is dropped at intake
and never reaches the table. Its delivered columns carry `accessorKey`
stamped from `field`. So a `name`-spelled `object-grid` column does not
render today either — that is objectui#5352's open question, unchanged by
this release.

Only the directly-authored `data-table` node narrows here.

**How the break presents, so you can recognise it:** the column is not dropped
and nothing is thrown — its header still renders over blank cells, and
neighbouring columns are unaffected. If a table's header row looks right but one
column's cells are empty, check that column's key spelling first.

The two published skill guides that taught the `name` spelling
(`skills/objectui/guides/data-integration.md`, `schema-expressions.md`) migrate
in this same release, so the platform never refuses a spelling it still ships.

Graded `minor`, not `patch`: this narrows the accepted input set, which is a
breaking change for any author who used the tolerated spelling. It is not
`major` per this repo's fixed-group convention (objectui's own breaking changes
ship as `minor`; the group's major tracks `@objectstack`).
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,11 +40,24 @@
* empty state. The correction is verified against the renderer rather
* than against a reading of it.
*
* The `columns` entries in these examples are deliberately untouched: their
* `{ name, label }` spelling is the separate open question on objectui#5120
* (the undeclared `col.name` / `col.label` alias at `data-table.tsx:776-777`),
* parked with the maintainer. This file pins the BINDING only, and stays true
* whichever way that one lands — nothing below asserts a column key spelling.
* ⚠️ COLUMN SPELLING — this file is NOT spelling-independent, and an earlier
* revision of this docblock said it was. It claimed the file "pins the BINDING
* only … nothing below asserts a column key spelling". That was false and was
* filed as objectui#5479: the BEHAVIOUR assertions below are on rendered CELL
* TEXT, and a cell only has text when its column's accessor resolves — so they
* transitively pin the accessor spelling the guides use. A docblock asserting
* independence over assertions that were not independent is what made
* objectui#5120's last step invisible when the family was ruled on.
*
* Both aliases have since retired — `label` in objectui#5351, `name` in
* objectui#5120 — and the two guides' `data-table` columns migrated to the
* declared `{ header, accessorKey }` in the same commit as the `name`
* retirement, because these blocks are lifted and rendered at run time and
* would otherwise go blank. That coupling is the point, not a defect: the
* guides are executable fixtures, so the instruction corpus cannot drift from
* the runtime without this file going red. Keep it that way — if a future
* change makes the adapter's accepted key set narrower again, migrate the
* guides in the SAME commit.
*/

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
Expand DownExpand Up@@ -162,6 +175,31 @@ describe('skill guides — no `data-table` example is bound with `bind` (#5126,
expect(offenders).toEqual([]);
});

it('no `data-table` example spells a column with the retired `name` (#5120)', () => {
// objectui#5120 retired `accessorKey: col.accessorKey || col.name` on the
// adapter. These blocks are RENDERED below, so a guide that regressed to
// `name` would already fail — but only as "expected [] to equal
// ['Ada Lovelace', …]", which names neither the key nor the card. This
// assertion is here to make the failure SAY what broke, and to hold the
// corpus even if the behaviour legs are ever narrowed.
for (const rel of GUIDE_PATHS) {
const md = readGuide(rel);
for (const node of blocksOfType(md, 'data-table')) {
const columns = (node.columns ?? []) as Record<string, unknown>[];
for (const col of columns) {
expect(
'name' in col,
`${rel}: a data-table column spells the retired \`name\`; the declared key is \`accessorKey\` (objectui#5120)`,
).toBe(false);
expect(
'accessorKey' in col,
`${rel}: a data-table column is missing the declared \`accessorKey\` (objectui#5120)`,
).toBe(true);
}
}
}
});

it('no guide claims a table component calls useDataScope', () => {
for (const rel of GUIDE_PATHS) {
const md = readGuide(rel);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,9 +57,16 @@ const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, '../../../..');
const skillsRoot = path.join(repoRoot, 'skills/objectui');

// The DECLARED column keys (`TableColumn`: `header` + `accessorKey`). These
// mirror the two published guides' `data-table` example, which is why they
// moved when the guides did: objectui#5120 retired the adapter's undeclared
// `col.name` alias, so the `{ name, label }` spelling this fixture used to
// carry now resolves no accessor and every cell below would read ''. The rows
// keep `name`/`email` as their own DATA keys — that is what `accessorKey`
// points AT, and it is unrelated to the column vocabulary.
const COLUMNS = [
{ name: 'name', label: 'Name' },
{ name: 'email', label: 'Email' },
{ header: 'Name', accessorKey: 'name' },
{ header: 'Email', accessorKey: 'email' },
];
const ROWS = [
{ name: 'Ada Lovelace', email: 'ada@example.com' },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,8 +7,9 @@
*/

/**
* `data-table` reads the DECLARED `header`, not the undeclared `label`
* (objectui#5351).
* `data-table` reads the DECLARED `header` and `accessorKey`, and NOTHING else
* — not the undeclared `label` (objectui#5351), not the undeclared `name`
* (objectui#5120).
*
* `TableColumn` (`packages/types/src/data-display.ts`) declares `header: string`
* and `accessorKey: string`. It declares neither `label` nor `name`. The
Expand All@@ -26,14 +27,39 @@
* purpose. Metadata vocabulary in, adapter vocabulary out; one translation, one
* place — and that place is each producer, never here.
*
* SCOPE. The `label` alias retires here; the `name` alias is HELD, and the last
* describe below pins it as still-read so the hold cannot be mistaken for the
* retirement having happened. Two published skill guides teach a directly
* authored `data-table` whose columns are spelled `{ name, label }`, and
* SCOPE. `label` retired in objectui#5351; `name` retires here, closing the
* family. The hold that kept `name` alive was never about the producers — all
* three resolve `accessorKey` themselves, measured — but about the INSTRUCTION
* CORPUS: two published skill guides taught a directly authored `data-table`
* whose columns were spelled `{ name, label }`, and
* `skill-guide-data-table-binding.test.tsx` renders those blocks straight out of
* the guide files — so `name` cannot retire until the instruction corpus moves.
* That is objectui#5120's remaining step and is nobody's to take unbidden:
* `skills/**` is a customer-published surface with its own owning seat.
* the guide files. Both guides migrated to `{ header, accessorKey }` in the same
* commit as this retirement, which is the only ordering in which the platform
* never refuses a spelling it still ships.
*
* WHAT NARROWS, precisely. A `{ name, label }` column arriving through
* `ObjectDataTable`, `RelatedList` or `ObjectGrid` is UNAFFECTED — but for two
* different reasons, and conflating them is a mistake this card's own ruling
* already made once:
*
* - `ObjectDataTable` and `RelatedList` RESOLVE the legacy spelling, stamping
* `accessorKey` from `columnIdentity(col)` before delivery.
* - `ObjectGrid` does NOT fold. Since objectui#5068 it REFUSES undeclared
* spellings at intake — `resolvesToDataColumn` requires a string `field`
* (`columnSpellingDiagnostics.ts`), so a `name`-spelled entry is dropped
* before delivery and never reaches this adapter at all (before AND after
* this change; that silence is objectui#5352's territory). What it does
* deliver carries `accessorKey` stamped from `field`.
*
* The 2026-08-20 ruling's item 2 told ObjectGrid to "connect to the same
* `columnIdentity` resolution"; objectui#5478 measured that following it would
* have RE-WIDENED what #5068 narrowed. Stated here because the claim has been
* restated wrongly more than once, and a third repetition would settle it.
*
* Either way the adapter never sees a legacy spelling from a producer. What
* stops resolving is `name` on a column authored DIRECTLY onto a `data-table`
* node. That is the whole blast radius, and it is pinned below in both
* directions.
*
* The two aliases were DIFFERENT failure classes, which is why the cards were
* filed apart: an unresolved `accessorKey` gives blank cells under a live
Expand DownExpand Up@@ -79,24 +105,52 @@ describe('data-table columns — the declared keys render (unchanged)', () => {
});
});

describe('data-table columns — the `name` alias is still read, and that is a HOLD (#5120)', () => {
it('still resolves an accessor from the undeclared `name`', () => {
// NOT an endorsement — a receipt. The 2026-08-20 ruling retires this limb;
// what stops it today is that two published skill guides teach it and
// `skill-guide-data-table-binding.test.tsx` renders their bytes. Pinning the
// CURRENT behaviour means the day the guides move, this test goes red and
// names itself as the thing to delete, instead of the retirement quietly
// never happening.
describe('data-table columns — the undeclared `name` alias is retired (#5120)', () => {
it('does not resolve an accessor from `name`', () => {
// The narrowing this card ships, and the receipt that replaces the HOLD pin
// that stood here while the instruction corpus still taught `name`.
// A DIFFERENT failure class from #5351's: the header is fine and the CELLS
// are what go blank.
renderTable([{ header: 'Stage', name: 'stage' }]);
expect(bodyCells()).toEqual(['Won', 'Lost']);
expect(headers()).toEqual(['Stage']);
expect(bodyCells()).toEqual(['', '']);
});

it('keeps an authored `accessorKey` ahead of a divergent `name`', () => {
it('keeps an authored `accessorKey` winning over a divergent `name`', () => {
// Precedence, unchanged and load bearing: columns can arrive in the table
// library's own shape, and those must not be second-guessed.
// library's own shape, and those must not be second-guessed. This passed
// before the retirement too — it is here so the two directions cannot drift.
renderTable([{ header: 'Stage', accessorKey: 'stage', name: 'nonsense' }]);
expect(bodyCells()).toEqual(['Won', 'Lost']);
});

it('LEGIBILITY: an unresolvable column keeps its header and spares its neighbour', () => {
// Measured, not assumed, and deliberately pinned as the SHAPE OF THE BREAK:
// the column is not dropped and nothing throws — a live header sits over
// blank cells while the declared neighbour is untouched. This is the failure
// an author who kept the legacy spelling now gets, and it is the same
// illegible shape objectui#5349 measures against.
renderTable([
{ header: 'Stage', name: 'stage' },
{ header: 'Id', accessorKey: 'id' },
]);
expect(headers()).toEqual(['Stage', 'Id']);
expect(bodyCells()).toEqual(['', '1', '', '2']);
});

it('a PRODUCER-resolved column is unaffected — the narrowing is adapter-only', () => {
// The blast-radius bound in one assertion: whatever a producer delivers
// already carries a declared `accessorKey`, so the retirement cannot reach
// it. ObjectDataTable and RelatedList get there by RESOLVING `name` through
// `columnIdentity`; ObjectGrid gets there by REFUSING undeclared spellings
// at intake and stamping from `field` (see the header — the two routes are
// not the same mechanism). Simulating the hand-off here keeps this file
// honest about what the retirement does and does not break, without
// importing a plugin package.
renderTable([{ header: 'Stage', name: 'stage', accessorKey: 'stage' }]);
expect(headers()).toEqual(['Stage']);
expect(bodyCells()).toEqual(['Won', 'Lost']);
});
});

describe('data-table columns — the undeclared `label` alias is retired (#5351)', () => {
Expand Down
41 changes: 23 additions & 18 deletions packages/components/src/renderers/complex/data-table.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -832,9 +832,9 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
if (nonArrayDataMessage) console.warn(nonArrayDataMessage);
}, [nonArrayDataMessage]);

// The adapter reads the column keys `TableColumn` DECLARES. The `label`
// alias is gone (objectui#5351); the `name` alias is HELD, and the hold is
// deliberate and documented rather than an oversight.
// The adapter reads ONLY the column keys `TableColumn` DECLARES. Both
// undeclared aliases are now retired: `label` (objectui#5351) and `name`
// (objectui#5120, this change).
//
// These two lines used to normalize each column as
// `header: col.header || col.label` and
Expand All@@ -846,28 +846,33 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
// ruling settled the direction for the whole family: retire the consumer-side
// alias, unify the producers.
//
// `header` has retired. Where its translation went — `columnHeader` in
// `@object-ui/core`, called by each producer before delivery:
// Where the translation went — `columnIdentity` / `columnHeader` in
// `@object-ui/core`, called by each producer BEFORE delivery:
// `ObjectDataTable.normalizeColumns` (`@object-ui/plugin-dashboard`)
// `RelatedList.normalizeColumn` (`@object-ui/plugin-detail`)
// `ObjectGrid.generateColumns` (`@object-ui/plugin-grid`, since #5068)
// Metadata vocabulary in, adapter vocabulary out; one translation, one place.
// Metadata vocabulary in, adapter vocabulary out; one translation, one place —
// and that place is each producer, never here. A `{ name, label }` column
// arriving through any of the three still renders: its producer resolved the
// identity into `accessorKey` before the adapter ever saw it. What no longer
// resolves is `name` on a column authored DIRECTLY onto a `data-table` node,
// which is the accepted-set narrowing objectui#5120 rules and ships.
//
// `accessorKey || col.name` STAYS, pending objectui#5120's remaining step. It
// is not that the producers still need it — all three resolve `accessorKey`
// themselves, measured — but that two PUBLISHED skill guides teach a directly
// authored `data-table` whose columns are spelled `{ name, label }`:
// The instruction corpus moved in the SAME commit, which is the whole reason
// this step could be taken: `skill-guide-data-table-binding.test.tsx` lifts the
// fenced JSON out of the published guides at run time and renders it, so the
// guides are executable fixtures rather than prose. Both now teach
// `{ header, accessorKey }`:
// skills/objectui/guides/data-integration.md
// skills/objectui/guides/schema-expressions.md
// `skill-guide-data-table-binding.test.tsx` lifts those blocks out of the real
// files at run time and renders them, so retiring `name` here turns that gate
// red until the guides move. Retiring the runtime ahead of the instruction
// would leave the platform refusing a spelling it still ships, and the failure
// it teaches into is the illegible one below: a header over blank cells.
// Retiring the runtime ahead of the instruction would have left the platform
// refusing a spelling it still shipped, and the failure it teaches into is the
// illegible one: a header over blank cells (pinned in
// `data-table-declared-column-keys.test.tsx`).
const initialColumns = useMemo(() => {
return rawColumns.map((col: any) => ({
...col,
accessorKey: col.accessorKey || col.name,
accessorKey: col.accessorKey,
}));
}, [rawColumns]);

Expand All@@ -876,10 +881,10 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
const widths: Record<string, number> = {};
// Spelled identically to `initialColumns` above — the auto-width pass must
// measure the SAME columns the table renders, so the two reads move
// together (objectui#5351 retired `header`'s alias; `name`'s is held).
// together (objectui#5351 retired `header`'s alias, objectui#5120 `name`'s).
const cols = rawColumns.map((col: any) => ({
header: col.header,
accessorKey: col.accessorKey || col.name,
accessorKey: col.accessorKey,
width: col.width,
fitContent: col.fitContent,
}));
Expand Down
4 changes: 2 additions & 2 deletions skills/objectui/guides/data-integration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,8 +217,8 @@ through is measured, with its open-question caveat, in
{ "name": "Grace Hopper", "email": "grace@example.com" }
],
"columns": [
{ "name": "name", "label": "Name" },
{ "name": "email", "label": "Email" }
{ "header": "Name", "accessorKey": "name" },
{ "header": "Email", "accessorKey": "email" }
]
}
```
Expand Down
4 changes: 2 additions & 2 deletions skills/objectui/guides/schema-expressions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -421,8 +421,8 @@ and is blank is the whole failure.
{ "name": "Grace Hopper", "email": "grace@example.com" }
],
"columns": [
{ "name": "name", "label": "Name" },
{ "name": "email", "label": "Email" }
{ "header": "Name", "accessorKey": "name" },
{ "header": "Email", "accessorKey": "email" }
]
}
```
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .changeset/5120-retire-data-table-name-alias.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
---
'@object-ui/components': minor
---

**Breaking for authored metadata:** a `data-table` column spelled `name` no
longer resolves its cells. Use the declared `accessorKey`.

`data-table`'s column normalization used to read
`accessorKey: col.accessorKey || col.name` — but `TableColumn`
(`@object-ui/types`) declares only `accessorKey`, never `name`. The declared
surface admitted one spelling while the runtime admitted two, which is the
second de-facto contract AGENTS.md #0.1 forbids. The maintainer ruling of
2026-08-20 settled the direction for the whole family: retire the consumer-side
alias, translate at the producers. `label` → `header` retired first
(objectui#5351); this retires `name` → `accessorKey` and closes the family.

**Who is affected — a column authored DIRECTLY onto a `data-table` node:**

```json
{ "type": "data-table",
"columns": [{ "name": "email", "label": "Email" }] } // ← was tolerated
```

becomes

```json
{ "type": "data-table",
"columns": [{ "header": "Email", "accessorKey": "email" }] }
```

**Who is NOT affected.** Columns reaching the table through `object-data-table`,
a detail view's `related[]` list, or `object-grid` are unchanged — the adapter
never sees a legacy spelling from any of them. The reason differs by producer,
and the difference matters if you are debugging one:

- `object-data-table` and a detail view's `related[]` list **resolve** the
legacy spelling before delivery, stamping `accessorKey` from `name` (via
`columnIdentity`). A `name`-spelled column keeps working there.
- `object-grid` **refuses** it instead: since objectui#5068 an authored column
must spell the declared `field`, and one that does not is dropped at intake
and never reaches the table. Its delivered columns carry `accessorKey`
stamped from `field`. So a `name`-spelled `object-grid` column does not
render today either — that is objectui#5352's open question, unchanged by
this release.

Only the directly-authored `data-table` node narrows here.

**How the break presents, so you can recognise it:** the column is not dropped
and nothing is thrown — its header still renders over blank cells, and
neighbouring columns are unaffected. If a table's header row looks right but one
column's cells are empty, check that column's key spelling first.

The two published skill guides that taught the `name` spelling
(`skills/objectui/guides/data-integration.md`, `schema-expressions.md`) migrate
in this same release, so the platform never refuses a spelling it still ships.

Graded `minor`, not `patch`: this narrows the accepted input set, which is a
breaking change for any author who used the tolerated spelling. It is not
`major` per this repo's fixed-group convention (objectui's own breaking changes
ship as `minor`; the group's major tracks `@objectstack`).
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,11 +40,24 @@
* empty state. The correction is verified against the renderer rather
* than against a reading of it.
*
* The `columns` entries in these examples are deliberately untouched: their
* `{ name, label }` spelling is the separate open question on objectui#5120
* (the undeclared `col.name` / `col.label` alias at `data-table.tsx:776-777`),
* parked with the maintainer. This file pins the BINDING only, and stays true
* whichever way that one lands — nothing below asserts a column key spelling.
* ⚠️ COLUMN SPELLING — this file is NOT spelling-independent, and an earlier
* revision of this docblock said it was. It claimed the file "pins the BINDING
* only … nothing below asserts a column key spelling". That was false and was
* filed as objectui#5479: the BEHAVIOUR assertions below are on rendered CELL
* TEXT, and a cell only has text when its column's accessor resolves — so they
* transitively pin the accessor spelling the guides use. A docblock asserting
* independence over assertions that were not independent is what made
* objectui#5120's last step invisible when the family was ruled on.
*
* Both aliases have since retired — `label` in objectui#5351, `name` in
* objectui#5120 — and the two guides' `data-table` columns migrated to the
* declared `{ header, accessorKey }` in the same commit as the `name`
* retirement, because these blocks are lifted and rendered at run time and
* would otherwise go blank. That coupling is the point, not a defect: the
* guides are executable fixtures, so the instruction corpus cannot drift from
* the runtime without this file going red. Keep it that way — if a future
* change makes the adapter's accepted key set narrower again, migrate the
* guides in the SAME commit.
*/

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
Expand DownExpand Up@@ -162,6 +175,31 @@ describe('skill guides — no `data-table` example is bound with `bind` (#5126,
expect(offenders).toEqual([]);
});

it('no `data-table` example spells a column with the retired `name` (#5120)', () => {
// objectui#5120 retired `accessorKey: col.accessorKey || col.name` on the
// adapter. These blocks are RENDERED below, so a guide that regressed to
// `name` would already fail — but only as "expected [] to equal
// ['Ada Lovelace', …]", which names neither the key nor the card. This
// assertion is here to make the failure SAY what broke, and to hold the
// corpus even if the behaviour legs are ever narrowed.
for (const rel of GUIDE_PATHS) {
const md = readGuide(rel);
for (const node of blocksOfType(md, 'data-table')) {
const columns = (node.columns ?? []) as Record<string, unknown>[];
for (const col of columns) {
expect(
'name' in col,
`${rel}: a data-table column spells the retired \`name\`; the declared key is \`accessorKey\` (objectui#5120)`,
).toBe(false);
expect(
'accessorKey' in col,
`${rel}: a data-table column is missing the declared \`accessorKey\` (objectui#5120)`,
).toBe(true);
}
}
}
});

it('no guide claims a table component calls useDataScope', () => {
for (const rel of GUIDE_PATHS) {
const md = readGuide(rel);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,9 +57,16 @@ const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, '../../../..');
const skillsRoot = path.join(repoRoot, 'skills/objectui');

// The DECLARED column keys (`TableColumn`: `header` + `accessorKey`). These
// mirror the two published guides' `data-table` example, which is why they
// moved when the guides did: objectui#5120 retired the adapter's undeclared
// `col.name` alias, so the `{ name, label }` spelling this fixture used to
// carry now resolves no accessor and every cell below would read ''. The rows
// keep `name`/`email` as their own DATA keys — that is what `accessorKey`
// points AT, and it is unrelated to the column vocabulary.
const COLUMNS = [
{ name: 'name', label: 'Name' },
{ name: 'email', label: 'Email' },
{ header: 'Name', accessorKey: 'name' },
{ header: 'Email', accessorKey: 'email' },
];
const ROWS = [
{ name: 'Ada Lovelace', email: 'ada@example.com' },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,8 +7,9 @@
*/

/**
* `data-table` reads the DECLARED `header`, not the undeclared `label`
* (objectui#5351).
* `data-table` reads the DECLARED `header` and `accessorKey`, and NOTHING else
* — not the undeclared `label` (objectui#5351), not the undeclared `name`
* (objectui#5120).
*
* `TableColumn` (`packages/types/src/data-display.ts`) declares `header: string`
* and `accessorKey: string`. It declares neither `label` nor `name`. The
Expand All@@ -26,14 +27,39 @@
* purpose. Metadata vocabulary in, adapter vocabulary out; one translation, one
* place — and that place is each producer, never here.
*
* SCOPE. The `label` alias retires here; the `name` alias is HELD, and the last
* describe below pins it as still-read so the hold cannot be mistaken for the
* retirement having happened. Two published skill guides teach a directly
* authored `data-table` whose columns are spelled `{ name, label }`, and
* SCOPE. `label` retired in objectui#5351; `name` retires here, closing the
* family. The hold that kept `name` alive was never about the producers — all
* three resolve `accessorKey` themselves, measured — but about the INSTRUCTION
* CORPUS: two published skill guides taught a directly authored `data-table`
* whose columns were spelled `{ name, label }`, and
* `skill-guide-data-table-binding.test.tsx` renders those blocks straight out of
* the guide files — so `name` cannot retire until the instruction corpus moves.
* That is objectui#5120's remaining step and is nobody's to take unbidden:
* `skills/**` is a customer-published surface with its own owning seat.
* the guide files. Both guides migrated to `{ header, accessorKey }` in the same
* commit as this retirement, which is the only ordering in which the platform
* never refuses a spelling it still ships.
*
* WHAT NARROWS, precisely. A `{ name, label }` column arriving through
* `ObjectDataTable`, `RelatedList` or `ObjectGrid` is UNAFFECTED — but for two
* different reasons, and conflating them is a mistake this card's own ruling
* already made once:
*
* - `ObjectDataTable` and `RelatedList` RESOLVE the legacy spelling, stamping
* `accessorKey` from `columnIdentity(col)` before delivery.
* - `ObjectGrid` does NOT fold. Since objectui#5068 it REFUSES undeclared
* spellings at intake — `resolvesToDataColumn` requires a string `field`
* (`columnSpellingDiagnostics.ts`), so a `name`-spelled entry is dropped
* before delivery and never reaches this adapter at all (before AND after
* this change; that silence is objectui#5352's territory). What it does
* deliver carries `accessorKey` stamped from `field`.
*
* The 2026-08-20 ruling's item 2 told ObjectGrid to "connect to the same
* `columnIdentity` resolution"; objectui#5478 measured that following it would
* have RE-WIDENED what #5068 narrowed. Stated here because the claim has been
* restated wrongly more than once, and a third repetition would settle it.
*
* Either way the adapter never sees a legacy spelling from a producer. What
* stops resolving is `name` on a column authored DIRECTLY onto a `data-table`
* node. That is the whole blast radius, and it is pinned below in both
* directions.
*
* The two aliases were DIFFERENT failure classes, which is why the cards were
* filed apart: an unresolved `accessorKey` gives blank cells under a live
Expand DownExpand Up@@ -79,24 +105,52 @@ describe('data-table columns — the declared keys render (unchanged)', () => {
});
});

describe('data-table columns — the `name` alias is still read, and that is a HOLD (#5120)', () => {
it('still resolves an accessor from the undeclared `name`', () => {
// NOT an endorsement — a receipt. The 2026-08-20 ruling retires this limb;
// what stops it today is that two published skill guides teach it and
// `skill-guide-data-table-binding.test.tsx` renders their bytes. Pinning the
// CURRENT behaviour means the day the guides move, this test goes red and
// names itself as the thing to delete, instead of the retirement quietly
// never happening.
describe('data-table columns — the undeclared `name` alias is retired (#5120)', () => {
it('does not resolve an accessor from `name`', () => {
// The narrowing this card ships, and the receipt that replaces the HOLD pin
// that stood here while the instruction corpus still taught `name`.
// A DIFFERENT failure class from #5351's: the header is fine and the CELLS
// are what go blank.
renderTable([{ header: 'Stage', name: 'stage' }]);
expect(bodyCells()).toEqual(['Won', 'Lost']);
expect(headers()).toEqual(['Stage']);
expect(bodyCells()).toEqual(['', '']);
});

it('keeps an authored `accessorKey` ahead of a divergent `name`', () => {
it('keeps an authored `accessorKey` winning over a divergent `name`', () => {
// Precedence, unchanged and load bearing: columns can arrive in the table
// library's own shape, and those must not be second-guessed.
// library's own shape, and those must not be second-guessed. This passed
// before the retirement too — it is here so the two directions cannot drift.
renderTable([{ header: 'Stage', accessorKey: 'stage', name: 'nonsense' }]);
expect(bodyCells()).toEqual(['Won', 'Lost']);
});

it('LEGIBILITY: an unresolvable column keeps its header and spares its neighbour', () => {
// Measured, not assumed, and deliberately pinned as the SHAPE OF THE BREAK:
// the column is not dropped and nothing throws — a live header sits over
// blank cells while the declared neighbour is untouched. This is the failure
// an author who kept the legacy spelling now gets, and it is the same
// illegible shape objectui#5349 measures against.
renderTable([
{ header: 'Stage', name: 'stage' },
{ header: 'Id', accessorKey: 'id' },
]);
expect(headers()).toEqual(['Stage', 'Id']);
expect(bodyCells()).toEqual(['', '1', '', '2']);
});

it('a PRODUCER-resolved column is unaffected — the narrowing is adapter-only', () => {
// The blast-radius bound in one assertion: whatever a producer delivers
// already carries a declared `accessorKey`, so the retirement cannot reach
// it. ObjectDataTable and RelatedList get there by RESOLVING `name` through
// `columnIdentity`; ObjectGrid gets there by REFUSING undeclared spellings
// at intake and stamping from `field` (see the header — the two routes are
// not the same mechanism). Simulating the hand-off here keeps this file
// honest about what the retirement does and does not break, without
// importing a plugin package.
renderTable([{ header: 'Stage', name: 'stage', accessorKey: 'stage' }]);
expect(headers()).toEqual(['Stage']);
expect(bodyCells()).toEqual(['Won', 'Lost']);
});
});

describe('data-table columns — the undeclared `label` alias is retired (#5351)', () => {
Expand Down
41 changes: 23 additions & 18 deletions packages/components/src/renderers/complex/data-table.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -832,9 +832,9 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
if (nonArrayDataMessage) console.warn(nonArrayDataMessage);
}, [nonArrayDataMessage]);

// The adapter reads the column keys `TableColumn` DECLARES. The `label`
// alias is gone (objectui#5351); the `name` alias is HELD, and the hold is
// deliberate and documented rather than an oversight.
// The adapter reads ONLY the column keys `TableColumn` DECLARES. Both
// undeclared aliases are now retired: `label` (objectui#5351) and `name`
// (objectui#5120, this change).
//
// These two lines used to normalize each column as
// `header: col.header || col.label` and
Expand All@@ -846,28 +846,33 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
// ruling settled the direction for the whole family: retire the consumer-side
// alias, unify the producers.
//
// `header` has retired. Where its translation went — `columnHeader` in
// `@object-ui/core`, called by each producer before delivery:
// Where the translation went — `columnIdentity` / `columnHeader` in
// `@object-ui/core`, called by each producer BEFORE delivery:
// `ObjectDataTable.normalizeColumns` (`@object-ui/plugin-dashboard`)
// `RelatedList.normalizeColumn` (`@object-ui/plugin-detail`)
// `ObjectGrid.generateColumns` (`@object-ui/plugin-grid`, since #5068)
// Metadata vocabulary in, adapter vocabulary out; one translation, one place.
// Metadata vocabulary in, adapter vocabulary out; one translation, one place —
// and that place is each producer, never here. A `{ name, label }` column
// arriving through any of the three still renders: its producer resolved the
// identity into `accessorKey` before the adapter ever saw it. What no longer
// resolves is `name` on a column authored DIRECTLY onto a `data-table` node,
// which is the accepted-set narrowing objectui#5120 rules and ships.
//
// `accessorKey || col.name` STAYS, pending objectui#5120's remaining step. It
// is not that the producers still need it — all three resolve `accessorKey`
// themselves, measured — but that two PUBLISHED skill guides teach a directly
// authored `data-table` whose columns are spelled `{ name, label }`:
// The instruction corpus moved in the SAME commit, which is the whole reason
// this step could be taken: `skill-guide-data-table-binding.test.tsx` lifts the
// fenced JSON out of the published guides at run time and renders it, so the
// guides are executable fixtures rather than prose. Both now teach
// `{ header, accessorKey }`:
// skills/objectui/guides/data-integration.md
// skills/objectui/guides/schema-expressions.md
// `skill-guide-data-table-binding.test.tsx` lifts those blocks out of the real
// files at run time and renders them, so retiring `name` here turns that gate
// red until the guides move. Retiring the runtime ahead of the instruction
// would leave the platform refusing a spelling it still ships, and the failure
// it teaches into is the illegible one below: a header over blank cells.
// Retiring the runtime ahead of the instruction would have left the platform
// refusing a spelling it still shipped, and the failure it teaches into is the
// illegible one: a header over blank cells (pinned in
// `data-table-declared-column-keys.test.tsx`).
const initialColumns = useMemo(() => {
return rawColumns.map((col: any) => ({
...col,
accessorKey: col.accessorKey || col.name,
accessorKey: col.accessorKey,
}));
}, [rawColumns]);

Expand All@@ -876,10 +881,10 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
const widths: Record<string, number> = {};
// Spelled identically to `initialColumns` above — the auto-width pass must
// measure the SAME columns the table renders, so the two reads move
// together (objectui#5351 retired `header`'s alias; `name`'s is held).
// together (objectui#5351 retired `header`'s alias, objectui#5120 `name`'s).
const cols = rawColumns.map((col: any) => ({
header: col.header,
accessorKey: col.accessorKey || col.name,
accessorKey: col.accessorKey,
width: col.width,
fitContent: col.fitContent,
}));
Expand Down
4 changes: 2 additions & 2 deletions skills/objectui/guides/data-integration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,8 +217,8 @@ through is measured, with its open-question caveat, in
{ "name": "Grace Hopper", "email": "grace@example.com" }
],
"columns": [
{ "name": "name", "label": "Name" },
{ "name": "email", "label": "Email" }
{ "header": "Name", "accessorKey": "name" },
{ "header": "Email", "accessorKey": "email" }
]
}
```
Expand Down
4 changes: 2 additions & 2 deletions skills/objectui/guides/schema-expressions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -421,8 +421,8 @@ and is blank is the whole failure.
{ "name": "Grace Hopper", "email": "grace@example.com" }
],
"columns": [
{ "name": "name", "label": "Name" },
{ "name": "email", "label": "Email" }
{ "header": "Name", "accessorKey": "name" },
{ "header": "Email", "accessorKey": "email" }
]
}
```
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .changeset/5120-retire-data-table-name-alias.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
---
'@object-ui/components': minor
---

**Breaking for authored metadata:** a `data-table` column spelled `name` no
longer resolves its cells. Use the declared `accessorKey`.

`data-table`'s column normalization used to read
`accessorKey: col.accessorKey || col.name` — but `TableColumn`
(`@object-ui/types`) declares only `accessorKey`, never `name`. The declared
surface admitted one spelling while the runtime admitted two, which is the
second de-facto contract AGENTS.md #0.1 forbids. The maintainer ruling of
2026-08-20 settled the direction for the whole family: retire the consumer-side
alias, translate at the producers. `label` → `header` retired first
(objectui#5351); this retires `name` → `accessorKey` and closes the family.

**Who is affected — a column authored DIRECTLY onto a `data-table` node:**

```json
{ "type": "data-table",
"columns": [{ "name": "email", "label": "Email" }] } // ← was tolerated
```

becomes

```json
{ "type": "data-table",
"columns": [{ "header": "Email", "accessorKey": "email" }] }
```

**Who is NOT affected.** Columns reaching the table through `object-data-table`,
a detail view's `related[]` list, or `object-grid` are unchanged — the adapter
never sees a legacy spelling from any of them. The reason differs by producer,
and the difference matters if you are debugging one:

- `object-data-table` and a detail view's `related[]` list **resolve** the
legacy spelling before delivery, stamping `accessorKey` from `name` (via
`columnIdentity`). A `name`-spelled column keeps working there.
- `object-grid` **refuses** it instead: since objectui#5068 an authored column
must spell the declared `field`, and one that does not is dropped at intake
and never reaches the table. Its delivered columns carry `accessorKey`
stamped from `field`. So a `name`-spelled `object-grid` column does not
render today either — that is objectui#5352's open question, unchanged by
this release.

Only the directly-authored `data-table` node narrows here.

**How the break presents, so you can recognise it:** the column is not dropped
and nothing is thrown — its header still renders over blank cells, and
neighbouring columns are unaffected. If a table's header row looks right but one
column's cells are empty, check that column's key spelling first.

The two published skill guides that taught the `name` spelling
(`skills/objectui/guides/data-integration.md`, `schema-expressions.md`) migrate
in this same release, so the platform never refuses a spelling it still ships.

Graded `minor`, not `patch`: this narrows the accepted input set, which is a
breaking change for any author who used the tolerated spelling. It is not
`major` per this repo's fixed-group convention (objectui's own breaking changes
ship as `minor`; the group's major tracks `@objectstack`).
Original file line numberDiff line numberDiff line change
Expand Up@@ -40,11 +40,24 @@
* empty state. The correction is verified against the renderer rather
* than against a reading of it.
*
* The `columns` entries in these examples are deliberately untouched: their
* `{ name, label }` spelling is the separate open question on objectui#5120
* (the undeclared `col.name` / `col.label` alias at `data-table.tsx:776-777`),
* parked with the maintainer. This file pins the BINDING only, and stays true
* whichever way that one lands — nothing below asserts a column key spelling.
* ⚠️ COLUMN SPELLING — this file is NOT spelling-independent, and an earlier
* revision of this docblock said it was. It claimed the file "pins the BINDING
* only … nothing below asserts a column key spelling". That was false and was
* filed as objectui#5479: the BEHAVIOUR assertions below are on rendered CELL
* TEXT, and a cell only has text when its column's accessor resolves — so they
* transitively pin the accessor spelling the guides use. A docblock asserting
* independence over assertions that were not independent is what made
* objectui#5120's last step invisible when the family was ruled on.
*
* Both aliases have since retired — `label` in objectui#5351, `name` in
* objectui#5120 — and the two guides' `data-table` columns migrated to the
* declared `{ header, accessorKey }` in the same commit as the `name`
* retirement, because these blocks are lifted and rendered at run time and
* would otherwise go blank. That coupling is the point, not a defect: the
* guides are executable fixtures, so the instruction corpus cannot drift from
* the runtime without this file going red. Keep it that way — if a future
* change makes the adapter's accepted key set narrower again, migrate the
* guides in the SAME commit.
*/

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
Expand DownExpand Up@@ -162,6 +175,31 @@ describe('skill guides — no `data-table` example is bound with `bind` (#5126,
expect(offenders).toEqual([]);
});

it('no `data-table` example spells a column with the retired `name` (#5120)', () => {
// objectui#5120 retired `accessorKey: col.accessorKey || col.name` on the
// adapter. These blocks are RENDERED below, so a guide that regressed to
// `name` would already fail — but only as "expected [] to equal
// ['Ada Lovelace', …]", which names neither the key nor the card. This
// assertion is here to make the failure SAY what broke, and to hold the
// corpus even if the behaviour legs are ever narrowed.
for (const rel of GUIDE_PATHS) {
const md = readGuide(rel);
for (const node of blocksOfType(md, 'data-table')) {
const columns = (node.columns ?? []) as Record<string, unknown>[];
for (const col of columns) {
expect(
'name' in col,
`${rel}: a data-table column spells the retired \`name\`; the declared key is \`accessorKey\` (objectui#5120)`,
).toBe(false);
expect(
'accessorKey' in col,
`${rel}: a data-table column is missing the declared \`accessorKey\` (objectui#5120)`,
).toBe(true);
}
}
}
});

it('no guide claims a table component calls useDataScope', () => {
for (const rel of GUIDE_PATHS) {
const md = readGuide(rel);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,9 +57,16 @@ const here = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(here, '../../../..');
const skillsRoot = path.join(repoRoot, 'skills/objectui');

// The DECLARED column keys (`TableColumn`: `header` + `accessorKey`). These
// mirror the two published guides' `data-table` example, which is why they
// moved when the guides did: objectui#5120 retired the adapter's undeclared
// `col.name` alias, so the `{ name, label }` spelling this fixture used to
// carry now resolves no accessor and every cell below would read ''. The rows
// keep `name`/`email` as their own DATA keys — that is what `accessorKey`
// points AT, and it is unrelated to the column vocabulary.
const COLUMNS = [
{ name: 'name', label: 'Name' },
{ name: 'email', label: 'Email' },
{ header: 'Name', accessorKey: 'name' },
{ header: 'Email', accessorKey: 'email' },
];
const ROWS = [
{ name: 'Ada Lovelace', email: 'ada@example.com' },
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,8 +7,9 @@
*/

/**
* `data-table` reads the DECLARED `header`, not the undeclared `label`
* (objectui#5351).
* `data-table` reads the DECLARED `header` and `accessorKey`, and NOTHING else
* — not the undeclared `label` (objectui#5351), not the undeclared `name`
* (objectui#5120).
*
* `TableColumn` (`packages/types/src/data-display.ts`) declares `header: string`
* and `accessorKey: string`. It declares neither `label` nor `name`. The
Expand All@@ -26,14 +27,39 @@
* purpose. Metadata vocabulary in, adapter vocabulary out; one translation, one
* place — and that place is each producer, never here.
*
* SCOPE. The `label` alias retires here; the `name` alias is HELD, and the last
* describe below pins it as still-read so the hold cannot be mistaken for the
* retirement having happened. Two published skill guides teach a directly
* authored `data-table` whose columns are spelled `{ name, label }`, and
* SCOPE. `label` retired in objectui#5351; `name` retires here, closing the
* family. The hold that kept `name` alive was never about the producers — all
* three resolve `accessorKey` themselves, measured — but about the INSTRUCTION
* CORPUS: two published skill guides taught a directly authored `data-table`
* whose columns were spelled `{ name, label }`, and
* `skill-guide-data-table-binding.test.tsx` renders those blocks straight out of
* the guide files — so `name` cannot retire until the instruction corpus moves.
* That is objectui#5120's remaining step and is nobody's to take unbidden:
* `skills/**` is a customer-published surface with its own owning seat.
* the guide files. Both guides migrated to `{ header, accessorKey }` in the same
* commit as this retirement, which is the only ordering in which the platform
* never refuses a spelling it still ships.
*
* WHAT NARROWS, precisely. A `{ name, label }` column arriving through
* `ObjectDataTable`, `RelatedList` or `ObjectGrid` is UNAFFECTED — but for two
* different reasons, and conflating them is a mistake this card's own ruling
* already made once:
*
* - `ObjectDataTable` and `RelatedList` RESOLVE the legacy spelling, stamping
* `accessorKey` from `columnIdentity(col)` before delivery.
* - `ObjectGrid` does NOT fold. Since objectui#5068 it REFUSES undeclared
* spellings at intake — `resolvesToDataColumn` requires a string `field`
* (`columnSpellingDiagnostics.ts`), so a `name`-spelled entry is dropped
* before delivery and never reaches this adapter at all (before AND after
* this change; that silence is objectui#5352's territory). What it does
* deliver carries `accessorKey` stamped from `field`.
*
* The 2026-08-20 ruling's item 2 told ObjectGrid to "connect to the same
* `columnIdentity` resolution"; objectui#5478 measured that following it would
* have RE-WIDENED what #5068 narrowed. Stated here because the claim has been
* restated wrongly more than once, and a third repetition would settle it.
*
* Either way the adapter never sees a legacy spelling from a producer. What
* stops resolving is `name` on a column authored DIRECTLY onto a `data-table`
* node. That is the whole blast radius, and it is pinned below in both
* directions.
*
* The two aliases were DIFFERENT failure classes, which is why the cards were
* filed apart: an unresolved `accessorKey` gives blank cells under a live
Expand DownExpand Up@@ -79,24 +105,52 @@ describe('data-table columns — the declared keys render (unchanged)', () => {
});
});

describe('data-table columns — the `name` alias is still read, and that is a HOLD (#5120)', () => {
it('still resolves an accessor from the undeclared `name`', () => {
// NOT an endorsement — a receipt. The 2026-08-20 ruling retires this limb;
// what stops it today is that two published skill guides teach it and
// `skill-guide-data-table-binding.test.tsx` renders their bytes. Pinning the
// CURRENT behaviour means the day the guides move, this test goes red and
// names itself as the thing to delete, instead of the retirement quietly
// never happening.
describe('data-table columns — the undeclared `name` alias is retired (#5120)', () => {
it('does not resolve an accessor from `name`', () => {
// The narrowing this card ships, and the receipt that replaces the HOLD pin
// that stood here while the instruction corpus still taught `name`.
// A DIFFERENT failure class from #5351's: the header is fine and the CELLS
// are what go blank.
renderTable([{ header: 'Stage', name: 'stage' }]);
expect(bodyCells()).toEqual(['Won', 'Lost']);
expect(headers()).toEqual(['Stage']);
expect(bodyCells()).toEqual(['', '']);
});

it('keeps an authored `accessorKey` ahead of a divergent `name`', () => {
it('keeps an authored `accessorKey` winning over a divergent `name`', () => {
// Precedence, unchanged and load bearing: columns can arrive in the table
// library's own shape, and those must not be second-guessed.
// library's own shape, and those must not be second-guessed. This passed
// before the retirement too — it is here so the two directions cannot drift.
renderTable([{ header: 'Stage', accessorKey: 'stage', name: 'nonsense' }]);
expect(bodyCells()).toEqual(['Won', 'Lost']);
});

it('LEGIBILITY: an unresolvable column keeps its header and spares its neighbour', () => {
// Measured, not assumed, and deliberately pinned as the SHAPE OF THE BREAK:
// the column is not dropped and nothing throws — a live header sits over
// blank cells while the declared neighbour is untouched. This is the failure
// an author who kept the legacy spelling now gets, and it is the same
// illegible shape objectui#5349 measures against.
renderTable([
{ header: 'Stage', name: 'stage' },
{ header: 'Id', accessorKey: 'id' },
]);
expect(headers()).toEqual(['Stage', 'Id']);
expect(bodyCells()).toEqual(['', '1', '', '2']);
});

it('a PRODUCER-resolved column is unaffected — the narrowing is adapter-only', () => {
// The blast-radius bound in one assertion: whatever a producer delivers
// already carries a declared `accessorKey`, so the retirement cannot reach
// it. ObjectDataTable and RelatedList get there by RESOLVING `name` through
// `columnIdentity`; ObjectGrid gets there by REFUSING undeclared spellings
// at intake and stamping from `field` (see the header — the two routes are
// not the same mechanism). Simulating the hand-off here keeps this file
// honest about what the retirement does and does not break, without
// importing a plugin package.
renderTable([{ header: 'Stage', name: 'stage', accessorKey: 'stage' }]);
expect(headers()).toEqual(['Stage']);
expect(bodyCells()).toEqual(['Won', 'Lost']);
});
});

describe('data-table columns — the undeclared `label` alias is retired (#5351)', () => {
Expand Down
41 changes: 23 additions & 18 deletions packages/components/src/renderers/complex/data-table.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -832,9 +832,9 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
if (nonArrayDataMessage) console.warn(nonArrayDataMessage);
}, [nonArrayDataMessage]);

// The adapter reads the column keys `TableColumn` DECLARES. The `label`
// alias is gone (objectui#5351); the `name` alias is HELD, and the hold is
// deliberate and documented rather than an oversight.
// The adapter reads ONLY the column keys `TableColumn` DECLARES. Both
// undeclared aliases are now retired: `label` (objectui#5351) and `name`
// (objectui#5120, this change).
//
// These two lines used to normalize each column as
// `header: col.header || col.label` and
Expand All@@ -846,28 +846,33 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
// ruling settled the direction for the whole family: retire the consumer-side
// alias, unify the producers.
//
// `header` has retired. Where its translation went — `columnHeader` in
// `@object-ui/core`, called by each producer before delivery:
// Where the translation went — `columnIdentity` / `columnHeader` in
// `@object-ui/core`, called by each producer BEFORE delivery:
// `ObjectDataTable.normalizeColumns` (`@object-ui/plugin-dashboard`)
// `RelatedList.normalizeColumn` (`@object-ui/plugin-detail`)
// `ObjectGrid.generateColumns` (`@object-ui/plugin-grid`, since #5068)
// Metadata vocabulary in, adapter vocabulary out; one translation, one place.
// Metadata vocabulary in, adapter vocabulary out; one translation, one place —
// and that place is each producer, never here. A `{ name, label }` column
// arriving through any of the three still renders: its producer resolved the
// identity into `accessorKey` before the adapter ever saw it. What no longer
// resolves is `name` on a column authored DIRECTLY onto a `data-table` node,
// which is the accepted-set narrowing objectui#5120 rules and ships.
//
// `accessorKey || col.name` STAYS, pending objectui#5120's remaining step. It
// is not that the producers still need it — all three resolve `accessorKey`
// themselves, measured — but that two PUBLISHED skill guides teach a directly
// authored `data-table` whose columns are spelled `{ name, label }`:
// The instruction corpus moved in the SAME commit, which is the whole reason
// this step could be taken: `skill-guide-data-table-binding.test.tsx` lifts the
// fenced JSON out of the published guides at run time and renders it, so the
// guides are executable fixtures rather than prose. Both now teach
// `{ header, accessorKey }`:
// skills/objectui/guides/data-integration.md
// skills/objectui/guides/schema-expressions.md
// `skill-guide-data-table-binding.test.tsx` lifts those blocks out of the real
// files at run time and renders them, so retiring `name` here turns that gate
// red until the guides move. Retiring the runtime ahead of the instruction
// would leave the platform refusing a spelling it still ships, and the failure
// it teaches into is the illegible one below: a header over blank cells.
// Retiring the runtime ahead of the instruction would have left the platform
// refusing a spelling it still shipped, and the failure it teaches into is the
// illegible one: a header over blank cells (pinned in
// `data-table-declared-column-keys.test.tsx`).
const initialColumns = useMemo(() => {
return rawColumns.map((col: any) => ({
...col,
accessorKey: col.accessorKey || col.name,
accessorKey: col.accessorKey,
}));
}, [rawColumns]);

Expand All@@ -876,10 +881,10 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
const widths: Record<string, number> = {};
// Spelled identically to `initialColumns` above — the auto-width pass must
// measure the SAME columns the table renders, so the two reads move
// together (objectui#5351 retired `header`'s alias; `name`'s is held).
// together (objectui#5351 retired `header`'s alias, objectui#5120 `name`'s).
const cols = rawColumns.map((col: any) => ({
header: col.header,
accessorKey: col.accessorKey || col.name,
accessorKey: col.accessorKey,
width: col.width,
fitContent: col.fitContent,
}));
Expand Down
4 changes: 2 additions & 2 deletions skills/objectui/guides/data-integration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -217,8 +217,8 @@ through is measured, with its open-question caveat, in
{ "name": "Grace Hopper", "email": "grace@example.com" }
],
"columns": [
{ "name": "name", "label": "Name" },
{ "name": "email", "label": "Email" }
{ "header": "Name", "accessorKey": "name" },
{ "header": "Email", "accessorKey": "email" }
]
}
```
Expand Down
4 changes: 2 additions & 2 deletions skills/objectui/guides/schema-expressions.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -421,8 +421,8 @@ and is blank is the whole failure.
{ "name": "Grace Hopper", "email": "grace@example.com" }
],
"columns": [
{ "name": "name", "label": "Name" },
{ "name": "email", "label": "Email" }
{ "header": "Name", "accessorKey": "name" },
{ "header": "Email", "accessorKey": "email" }
]
}
```
Expand Down
Loading