From 7538f0ac32e21fa11dfefbed0e99638b280ec0fe Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 03:52:55 +0000 Subject: [PATCH 1/2] fix(components)!: data-table reads only the declared accessorKey MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Retire the undeclared `col.name` tolerance on the data-table adapter, and migrate the instruction corpus that taught it in the same commit. `data-table`'s column normalization read `accessorKey: col.accessorKey || col.name` at two sites, but `TableColumn` (`@object-ui/types`) declares only `accessorKey`. The declared surface admitted one spelling while the runtime admitted two — the second de-facto contract AGENTS.md #0.1 forbids. The maintainer ruling of 2026-08-20 settled the direction for the family: retire the consumer-side alias, translate at the producers. `label` retired first; this closes the family. The two published skill guides that taught `{ name, label }` columns migrate to the declared `{ header, accessorKey }` here, not in a follow-up. They are executable fixtures — `skill-guide-data-table-binding.test.tsx` lifts their fenced JSON at run time and renders it — so moving the runtime without the instruction would leave the platform refusing a spelling it still ships. Blast radius is adapter-only: columns arriving through ObjectDataTable, RelatedList or ObjectGrid are unaffected, because each producer folds `name` into `accessorKey` via `columnIdentity` before delivery. What narrows is a column authored directly onto a `data-table` node. Tests follow the change rather than being retrofitted to it: - the HOLD pin in `data-table-declared-column-keys.test.tsx` becomes a retirement pin, plus a legibility pin (live header over blank cells, neighbour untouched) and a producer-resolved pin bounding the radius; - the guide gate's docblock claimed it "stays true whichever way #5120 lands — nothing below asserts a column key spelling". That was false (objectui#5479): its assertions are on cell text, so they transitively pin the accessor spelling. Corrected, and an explicit spelling assertion added so the failure names the key and the card instead of diffing cell arrays; - `skill-guide-provider-envelope.test.tsx` carried a hardcoded `{ name, label }` COLUMNS fixture mirroring the guides; migrated with them. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB --- .../5120-retire-data-table-name-alias.md | 49 ++++++++++++ .../skill-guide-data-table-binding.test.tsx | 48 ++++++++++-- .../skill-guide-provider-envelope.test.tsx | 11 ++- .../data-table-declared-column-keys.test.tsx | 74 ++++++++++++++----- .../src/renderers/complex/data-table.tsx | 41 +++++----- skills/objectui/guides/data-integration.md | 4 +- skills/objectui/guides/schema-expressions.md | 4 +- 7 files changed, 182 insertions(+), 49 deletions(-) create mode 100644 .changeset/5120-retire-data-table-name-alias.md diff --git a/.changeset/5120-retire-data-table-name-alias.md b/.changeset/5120-retire-data-table-name-alias.md new file mode 100644 index 0000000000..789ef1a3e9 --- /dev/null +++ b/.changeset/5120-retire-data-table-name-alias.md @@ -0,0 +1,49 @@ +--- +'@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: those +producers already fold `name` into `accessorKey` via `columnIdentity` before +delivery, so the adapter never sees the legacy spelling. Only the +directly-authored node narrows. + +**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`). diff --git a/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx b/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx index ee25891fe1..421bca4d63 100644 --- a/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx +++ b/packages/components/src/__tests__/skill-guide-data-table-binding.test.tsx @@ -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'; @@ -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[]; + 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); diff --git a/packages/components/src/__tests__/skill-guide-provider-envelope.test.tsx b/packages/components/src/__tests__/skill-guide-provider-envelope.test.tsx index c93be1a4e8..c18e833f99 100644 --- a/packages/components/src/__tests__/skill-guide-provider-envelope.test.tsx +++ b/packages/components/src/__tests__/skill-guide-provider-envelope.test.tsx @@ -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' }, diff --git a/packages/components/src/renderers/complex/__tests__/data-table-declared-column-keys.test.tsx b/packages/components/src/renderers/complex/__tests__/data-table-declared-column-keys.test.tsx index 690cb8b0bd..c963e45fb9 100644 --- a/packages/components/src/renderers/complex/__tests__/data-table-declared-column-keys.test.tsx +++ b/packages/components/src/renderers/complex/__tests__/data-table-declared-column-keys.test.tsx @@ -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 @@ -26,14 +27,22 @@ * 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 — its producer + * folds `name` into `accessorKey` via `columnIdentity` before delivery, so the + * adapter never sees the legacy spelling. 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 @@ -79,24 +88,49 @@ 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. Producers fold `name` into + // `accessorKey` via `columnIdentity` BEFORE delivery (ObjectDataTable, + // RelatedList, ObjectGrid), so what reaches the adapter is already declared. + // Simulating that 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)', () => { diff --git a/packages/components/src/renderers/complex/data-table.tsx b/packages/components/src/renderers/complex/data-table.tsx index be0dc35457..7418fef471 100644 --- a/packages/components/src/renderers/complex/data-table.tsx +++ b/packages/components/src/renderers/complex/data-table.tsx @@ -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 @@ -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]); @@ -876,10 +881,10 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => { const widths: Record = {}; // 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, })); diff --git a/skills/objectui/guides/data-integration.md b/skills/objectui/guides/data-integration.md index 144ab29cfa..c108c9e43a 100644 --- a/skills/objectui/guides/data-integration.md +++ b/skills/objectui/guides/data-integration.md @@ -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" } ] } ``` diff --git a/skills/objectui/guides/schema-expressions.md b/skills/objectui/guides/schema-expressions.md index 0c9f63999f..367442a1b9 100644 --- a/skills/objectui/guides/schema-expressions.md +++ b/skills/objectui/guides/schema-expressions.md @@ -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" } ] } ``` From 8be9685863920cfb3c879f2dfe602ed3ec80c77b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 04:17:30 +0000 Subject: [PATCH 2/2] docs(components): ObjectGrid refuses the legacy spelling, it does not fold it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contract review on PR #6963 caught a mechanism mis-attribution. The blast-radius CONCLUSION was verified correct — columns arriving through ObjectDataTable, RelatedList or ObjectGrid are all unaffected by the `col.name` retirement — but the stated REASON held for only two of the three. - ObjectDataTable and RelatedList genuinely resolve the legacy spelling, stamping `accessorKey` from `columnIdentity(col)` before delivery. - ObjectGrid does NOT fold. Since #5068 it refuses undeclared spellings at intake: `resolvesToDataColumn` requires a non-empty string `field`, so a name-spelled entry is dropped before delivery and never reaches the adapter at all — before and after this PR. What it delivers carries `accessorKey` stamped from `field`. This matters beyond pedantry: the 2026-08-20 ruling's item 2 told ObjectGrid to "connect to the same columnIdentity resolution", and #5478 measured that following it literally would have re-widened exactly what #5068 narrowed. The claim has now been restated in the wrong direction by two separate seats. A third repetition is how it becomes settled fact, so it is corrected in the durable artifacts rather than only in a review thread. Corrected in the changeset (author-facing, and the one that would have shipped the false mechanism into release notes), and in the two test comments that carried it. Comment and changeset text only — no assertion, no source line, and no behaviour changes; the three affected pin files stay 44/44 green. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB --- .../5120-retire-data-table-name-alias.md | 19 +++++++-- .../data-table-declared-column-keys.test.tsx | 40 ++++++++++++++----- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/.changeset/5120-retire-data-table-name-alias.md b/.changeset/5120-retire-data-table-name-alias.md index 789ef1a3e9..e9397beb38 100644 --- a/.changeset/5120-retire-data-table-name-alias.md +++ b/.changeset/5120-retire-data-table-name-alias.md @@ -29,10 +29,21 @@ becomes ``` **Who is NOT affected.** Columns reaching the table through `object-data-table`, -a detail view's `related[]` list, or `object-grid` are unchanged: those -producers already fold `name` into `accessorKey` via `columnIdentity` before -delivery, so the adapter never sees the legacy spelling. Only the -directly-authored node narrows. +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 diff --git a/packages/components/src/renderers/complex/__tests__/data-table-declared-column-keys.test.tsx b/packages/components/src/renderers/complex/__tests__/data-table-declared-column-keys.test.tsx index c963e45fb9..fdb18db432 100644 --- a/packages/components/src/renderers/complex/__tests__/data-table-declared-column-keys.test.tsx +++ b/packages/components/src/renderers/complex/__tests__/data-table-declared-column-keys.test.tsx @@ -38,11 +38,28 @@ * never refuses a spelling it still ships. * * WHAT NARROWS, precisely. A `{ name, label }` column arriving through - * `ObjectDataTable`, `RelatedList` or `ObjectGrid` is UNAFFECTED — its producer - * folds `name` into `accessorKey` via `columnIdentity` before delivery, so the - * adapter never sees the legacy spelling. 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. + * `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 @@ -122,11 +139,14 @@ describe('data-table columns — the undeclared `name` alias is retired (#5120)' }); it('a PRODUCER-resolved column is unaffected — the narrowing is adapter-only', () => { - // The blast-radius bound in one assertion. Producers fold `name` into - // `accessorKey` via `columnIdentity` BEFORE delivery (ObjectDataTable, - // RelatedList, ObjectGrid), so what reaches the adapter is already declared. - // Simulating that hand-off here keeps this file honest about what the - // retirement does and does not break, without importing a plugin package. + // 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']);