From fcb434702ceb8558a31cee8d87b515620b4d5146 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 15:36:56 +0000 Subject: [PATCH 1/2] wip(plugin-dashboard): retire FieldMeta.referenceTo (objectui#6597) WIP checkpoint before the ablation proving the new tombstone is load-bearing. --- packages/plugin-dashboard/README.md | 10 +- .../plugin-dashboard/src/ObjectDataTable.tsx | 128 ++++++++++++------ ...ObjectDataTable.emitBoundary-6373.test.tsx | 78 +++++++++-- ...jectDataTable.overrideSource-6425.test.tsx | 67 ++++++++- .../plugin-dashboard/src/recordFields.tsx | 47 +++++-- 5 files changed, 252 insertions(+), 78 deletions(-) diff --git a/packages/plugin-dashboard/README.md b/packages/plugin-dashboard/README.md index 72dbae9a11..4e81d90cd1 100644 --- a/packages/plugin-dashboard/README.md +++ b/packages/plugin-dashboard/README.md @@ -486,10 +486,12 @@ object schema once and infers the renderer from the bound field: | `percent` | `0%` / `0.0%` formatted (honour `format`) | Author overrides always win — pass `type`, `format`, `options`, -`currency`, `referenceTo`, or your own `cell` function on a column to -bypass auto-detection. An explicit `currency` (ISO 4217 code, e.g. -`"EUR"`) wins over both the symbol inferred from `format` and the tenant -default currency. +`currency`, or your own `cell` function on a column to bypass +auto-detection. An explicit `currency` (ISO 4217 code, e.g. `"EUR"`) +wins over both the symbol inferred from `format` and the tenant default +currency. A lookup column's related-object target always comes from the +object schema's own field definition — there is no column-level override +for it (objectui#6597: measured no authoring story for one). ```jsonc { diff --git a/packages/plugin-dashboard/src/ObjectDataTable.tsx b/packages/plugin-dashboard/src/ObjectDataTable.tsx index 90cb9689b5..bf2a0190ea 100644 --- a/packages/plugin-dashboard/src/ObjectDataTable.tsx +++ b/packages/plugin-dashboard/src/ObjectDataTable.tsx @@ -168,6 +168,37 @@ interface NormalizedColumn { */ type ObjectDataTableRetiredDecimalsTombstone = { decimals?: never }; +/** + * ⭐ `referenceTo` — RETIRED from `FieldMeta` ITSELF (objectui#6597), the same + * mechanism and the same reason as {@link ObjectDataTableRetiredDecimalsTombstone} + * above — read that docblock for the full mechanics of why a hand-written + * tombstone, not the derived band, is what has to carry this refusal now that + * the key has left `keyof FieldMeta`. + * + * The verdict: `referenceTo` was documented by this package's README as an + * author-facing column override, and objectui#6425's ruling routed it to this + * card (⛔ "not declared as spelled") rather than deciding it, because the + * card that could declare or retire a HELD key cannot also be the card that + * MEASURES whether a real authoring story exists for it. objectui#6597 did + * that measurement: `LookupCellRenderer` resolves its lookup target from + * `field.reference_to` / `field.reference`, never `field.referenceTo`, and + * `computeLookupExpand` builds `$expand` from the OBJECT SCHEMA's field types, + * never from an authored column override — so the key reached nothing, HELD + * or not, pinned by the `referenceTo`-vs-`options` positive control in + * `ObjectDataTable.overrideSource-6425.test.tsx`. No authoring story survived + * the search either: `ObjectGrid`'s own relational-meta pass-through + * (`applyRelationalMeta`) copies `reference_to` / `reference` from the SCHEMA + * field def only, never from an authored override, and nothing in this repo's + * docs/examples/fixtures shows a table column pinning a lookup's target away + * from what its schema field already says. Under the maintainer's standing + * startup-stage rule (2026-08-27: no measured demand retires immediately, + * no transition window), that measurement selects RETIRE — the README line is + * withdrawn with it. ⭐ A future reader for the resolved reference target + * reads `reference_to` / `reference` off the schema field def — the spelling + * `LookupCellRenderer` and `computeLookupExpand` actually use — never this key. + */ +type ObjectDataTableRetiredReferenceToTombstone = { referenceTo?: never }; + export type EnrichedColumn = TableColumn /** HELD alias, objectui#5120 — see above. Not declared by `TableColumn`. */ @@ -176,7 +207,8 @@ export type EnrichedColumn = * a future `FieldMeta` member is tombstoned by default and has to be * adjudicated to escape. */ & { [K in Exclude]?: never } - & ObjectDataTableRetiredDecimalsTombstone; + & ObjectDataTableRetiredDecimalsTombstone + & ObjectDataTableRetiredReferenceToTombstone; /** * What this widget's column producer is allowed to READ off the AUTHORED @@ -209,9 +241,10 @@ export type EnrichedColumn = * {@link ObjectDataTableRetiredDecimalsTombstone} since objectui#6625 * retired the `FieldMeta` member itself and took the key out of the derived * band's pool; the verdict is unchanged, only its mechanism moved. - * - `referenceTo` — ⛔ NOT declared as spelled; still HELD, owned by - * objectui#6597 (the enforce-or-remove channel: fix the spelling chain so - * the promise becomes real, or withdraw the README line). + * - `referenceTo` — RETIRED (objectui#6597, enforce-or-remove: measured no + * authoring story for a column-level reference override — see the + * withdraw verdict above `ObjectDataTableRetiredReferenceToTombstone`). + * Refused by that tombstone the same way `decimals` is refused by its own. * * The shape itself is unchanged from what objectui#6461 landed for * `plugin-grid` (`ObjectGridColumnHolds` / `RetiredListColumnKey`); the @@ -225,9 +258,11 @@ export type EnrichedColumn = * NEW `FieldMeta` member is refused here on the day it is added, without * anyone remembering to extend a hand-written list. (Stated without a count on * purpose: it read "a seventh" while the type had eight members, and - * objectui#6625 has since retired one. The property is that ADDING is covered - * by derivation — ⚠️ REMOVING is not, which is why that card had to leave - * {@link ObjectDataTableRetiredDecimalsTombstone} behind by hand.) + * objectui#6625 and objectui#6597 have since retired two. The property is + * that ADDING is covered by derivation — ⚠️ REMOVING is not, which is why + * those cards each had to leave a hand-written tombstone behind — + * {@link ObjectDataTableRetiredDecimalsTombstone} and + * {@link ObjectDataTableRetiredReferenceToTombstone}.) * * ⚠️ It derives from the OVERRIDE VOCABULARY, not from the authored input * type, and that difference is forced rather than stylistic. `plugin-grid`'s @@ -270,28 +305,26 @@ export type EnrichedColumn = */ /** - * The undeclared-but-live override keys this producer still holds. One is - * left: objectui#6425's ruling (maintainer, 2026-08-27) declared `format` / + * The undeclared-but-live override keys this producer still holds. **None + * remain.** objectui#6425's ruling (maintainer, 2026-08-27) declared `format` / * `options` / `currency` on `TableColumn` itself (they are read via * `Pick` below, no hold needed) and retired `decimals` * outright — refused by {@link ObjectDataTableRetiredDecimalsTombstone} since * objectui#6625 retired the `FieldMeta` member that used to carry it into the - * derived band. + * derived band. `referenceTo` was the last hold — objectui#6425's ruling + * routed it to objectui#6597 rather than deciding it (⛔ "not declared as + * spelled"), and that card MEASURED no authoring story for a column-level + * reference override, so the maintainer's standing startup-stage rule + * (2026-08-27: no measured demand retires immediately) picked withdraw — + * refused by {@link ObjectDataTableRetiredReferenceToTombstone} the same way + * `decimals` is refused by its own tombstone. + * + * Kept as an interface — empty rather than deleted — because it is the + * documented anchor a future per-key ruling holds a new key onto, the same + * role it played for `format` / `options` / `currency` before objectui#6425 + * declared them and for `referenceTo` before objectui#6597 retired it. */ -export interface ObjectDataTableColumnHolds { - /** - * HELD — owned by objectui#6597, where objectui#6425's ruling routed it - * (⛔ NOT declared as spelled). Named as an author override by the package - * README but MEASURED with no reader on this path: `LookupCellRenderer` - * resolves its target from `reference_to` / `reference`, and - * `computeLookupExpand` builds `$expand` from the OBJECT SCHEMA's field - * types, never from this key. A promised-but-not-kept key: #6597 either - * fixes the spelling chain so the promise becomes real, or withdraws the - * README line — until it rules, the read stays held here, neither declared - * nor retired. - */ - referenceTo?: unknown; -} +export interface ObjectDataTableColumnHolds {} /** * The candidate keys this seam refuses — DERIVED from the override vocabulary, @@ -301,14 +334,18 @@ export interface ObjectDataTableColumnHolds { * folded below by `normalizeTableColumnType`) and, since objectui#6425's * ruling, `format` / `options` / `currency`. * - * ⚠️ `decimals` USED TO land here — that is how objectui#6425's ruling was - * enforced at the read site. It no longer can: objectui#6625 retired the - * `FieldMeta` member itself, so the key is out of this Exclude's POOL rather - * than out of its exclusion list, and a derived band cannot refuse a key that - * is not in what it derives from. The refusal is carried by - * {@link ObjectDataTableRetiredDecimalsTombstone} instead, intersected below. - * ⛔ Do not read this band's silence about `decimals` as a softening — the two - * artefacts together are the same verdict, unchanged since 2026-08-27. + * ⚠️ `decimals` and `referenceTo` USED TO land here — that is how objectui#6425's + * ruling was enforced for `decimals` at the read site, and how `referenceTo` + * stayed HELD (neither declared nor refused) until objectui#6597 ruled. Neither + * can any more: objectui#6625 and objectui#6597 each retired the `FieldMeta` + * member itself, so both keys are out of this Exclude's POOL rather than out of + * its exclusion list, and a derived band cannot refuse a key that is not in + * what it derives from. Their refusals are carried by + * {@link ObjectDataTableRetiredDecimalsTombstone} and + * {@link ObjectDataTableRetiredReferenceToTombstone} instead, intersected below. + * ⛔ Do not read this band's silence about either key as a softening — the + * tombstone and the band together are the same verdict, unchanged since each + * key's ruling. * * The pool is what shrank, so what THIS band still refuses is `name` and * `label` — both of them `FieldMeta` members with answers this seam already @@ -331,7 +368,11 @@ export type AuthoredColumnOverrides = & { [K in UnheldFieldMetaOverrideKey]?: never } /** RETIRED, objectui#6425's verdict — re-stated by hand because objectui#6625 * took the key out of the derived band's pool. See the tombstone's docblock. */ - & ObjectDataTableRetiredDecimalsTombstone; + & ObjectDataTableRetiredDecimalsTombstone + /** RETIRED, objectui#6597's verdict (enforce-or-remove, withdraw) — + * re-stated by hand for the same reason: the key left `keyof FieldMeta`, + * so the derived band can no longer reach it. See the tombstone's docblock. */ + & ObjectDataTableRetiredReferenceToTombstone; /** * Shared empty fallback for the resolved row list (objectui#4629). @@ -668,8 +709,8 @@ export const ObjectDataTable: React.FC = ({ schema, dataSo // headers automatically pick up i18n bundles. // // Each column is also enriched from the bound object schema — `options`, - // `referenceTo`, `format`, `currency`, `decimals` — and gets a `cell:` render - // function that delegates to `getCellRenderer` from `@object-ui/fields`. This + // `format`, `currency` — and gets a `cell:` render function that delegates + // to `getCellRenderer` from `@object-ui/fields`. This // produces the same type-aware rendering as ObjectGrid / list views and the // report viewer (Badge for select, link for lookup, ✓/✗ for boolean, // mailto:/tel: links, currency/percent/date formatting honouring the column's @@ -733,16 +774,24 @@ export const ObjectDataTable: React.FC = ({ schema, dataSo const authored: AuthoredColumnOverrides = col; // Build the shared FieldMeta (translated select options, resolved - // referenceTo / currency). Column-level props override the - // schema-derived values. Lookup fields just pass `referenceTo` through — - // the server expands them via `$expand` so the cell value is `{ id, name }`, - // which the lookup/user cell renderers handle natively. + // currency). Column-level props override the schema-derived values. + // Lookup fields need no override here for their relation target: the + // server expands them via `$expand` (built from the OBJECT SCHEMA's + // field types by `computeLookupExpand`, never from an authored column + // key) so the cell value arrives as `{ id, name }`, which the + // lookup/user cell renderers handle natively off the expanded record. // // `decimals` is NOT read here any more — RETIRED by objectui#6425's // ruling (maintainer, 2026-08-27): zero readers were measured for it // (`NumberCellRenderer` reads `scale`, `PercentCellRenderer` reads // `precision`), so the authored key never reached anything, and the // schema-derived value `buildFieldMeta` still resolves is untouched. + // + // `referenceTo` is NOT read here any more either — RETIRED by + // objectui#6597 (enforce-or-remove, withdraw): `LookupCellRenderer` + // resolves its target from `reference_to` / `reference`, never this + // spelling, so an authored `referenceTo` never reached anything on this + // path (measured, `ObjectDataTable.overrideSource-6425.test.tsx`). const fieldMeta = buildFieldMeta({ accessorKey: col.accessorKey, label: col.header, @@ -753,7 +802,6 @@ export const ObjectDataTable: React.FC = ({ schema, dataSo type: authored.type, format: authored.format, options: authored.options, - referenceTo: authored.referenceTo, currency: authored.currency, }, }); diff --git a/packages/plugin-dashboard/src/__tests__/ObjectDataTable.emitBoundary-6373.test.tsx b/packages/plugin-dashboard/src/__tests__/ObjectDataTable.emitBoundary-6373.test.tsx index 32129b7768..636bcf1d4b 100644 --- a/packages/plugin-dashboard/src/__tests__/ObjectDataTable.emitBoundary-6373.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/ObjectDataTable.emitBoundary-6373.test.tsx @@ -112,7 +112,13 @@ const HELD_ALIAS = 'name'; * `TableColumn` itself (an authored value passes through `{ ...col }` as * declared metadata; the producer still never writes them out of * `fieldMeta`), three remain undeclared (`decimals` retired outright, - * `referenceTo` held for objectui#6597, `label` objectui#5351's). + * `referenceTo` held for objectui#6597 at the time this ruling landed — that + * card has since measured no authoring story and retired it too, the same + * verdict as `decimals` — `label` objectui#5351's). The EMIT verdict for all + * three was already "retired" either way, so this array is unchanged by + * either later card: `decimals` and `referenceTo` moving from HELD/adjudicated + * to fully RETIRED `FieldMeta` members doesn't touch what this producer WRITES + * — it never wrote either from `fieldMeta` in the first place. */ const RETIRED_FROM_EMIT_UNDECLARED = ['label', 'referenceTo', 'decimals'] as const; const RETIRED_FROM_EMIT_DECLARED = ['options', 'format', 'currency'] as const; @@ -132,7 +138,12 @@ const accountSchema = { ], }, amount: { type: 'currency', label: 'Amount', currency: 'USD', scale: 2 }, - owner: { type: 'lookup', label: 'Owner', referenceTo: 'user' }, + // Canonical spec spelling (`reference`, not the retired `referenceTo` — + // objectui#6597) — this field's `type: 'lookup'` is what drives + // `computeLookupExpand` and the render below; `reference` documents the + // target for a reader, but nothing in this package's `FieldMeta` pipeline + // consumes it (retired end to end by this card). + owner: { type: 'lookup', label: 'Owner', reference: 'user' }, }, }; @@ -214,10 +225,11 @@ describe('ObjectDataTable emits only what the columns slot declares (#6373)', () // members unconditionally, so before this card every one of these keys // EXISTED on every emitted column — carrying `undefined` where the schema // said nothing, which is its own small lie about the shape. (It returned - // eight then and returns seven now: objectui#6625 retired `decimals` from - // `FieldMeta` itself, so that key can no longer be written from here even - // by accident. The verdict below is unchanged — it was already retired - // from the EMIT by this card.) + // eight originally, seven after objectui#6625 retired `decimals` from + // `FieldMeta` itself, and returns six now: objectui#6597 retired + // `referenceTo` from `FieldMeta` the same way. Neither key can be + // written from here even by accident any more. The verdict below is + // unchanged — both were already retired from the EMIT by this card.) for (const key of RETIRED) { expect(Object.keys(col), `${col.accessorKey}.${key}`).not.toContain(key); } @@ -236,8 +248,17 @@ describe('ObjectDataTable emits only what the columns slot declares (#6373)', () await waitFor(() => expect(screen.getByText('Acme')).toBeInTheDocument(), { timeout: 2000 }); expect(screen.getByText('Technology')).toBeInTheDocument(); expect(screen.queryByText('tech')).not.toBeInTheDocument(); - // `referenceTo` / the lookup road: the expanded record renders its display - // name, not the raw FK id. + // The lookup road: the expanded record renders its display name, not the + // raw FK id. Note this does NOT exercise `referenceTo` — retired end to + // end by objectui#6597, and never read on this path even before the + // retirement (`ObjectDataTable.overrideSource-6425.test.tsx` measured + // that). `computeLookupExpand` expands `owner` off the schema field's + // `type: 'lookup'` alone, and the value arrives pre-expanded as + // `{ id, name }`; `resolveLookupRecordName` falls back to + // `pickRecordDisplayName`'s generic `.name`/`.title` heuristic when no + // `refSchema` is available (`useRefObjectSchema(referenceTo)` needs the + // reference target, which this producer never supplies) — no + // `reference_to` / `reference` needed for that fallback to resolve `Ada`. expect(screen.getByText('Ada')).toBeInTheDocument(); expect(screen.queryByText('u1')).not.toBeInTheDocument(); }); @@ -303,13 +324,14 @@ describe("the emit type can FAIL — otherwise the annotation is decoration (#63 // `Omit` spans the retired members: `name` is // the held alias, `type` carries #5853's own refusal. Since the #6425 // ruling declared `format` / `options` / `currency` on `TableColumn`, the - // members still refused by the DERIVED tombstones are `label` and - // `referenceTo` — enough to keep this spread an error, and nothing but the - // tombstones refuses it. (`decimals` used to be a third; objectui#6625 - // retired the `FieldMeta` member, so it is no longer spanned by this - // `Omit` at all. Its own refusal is pinned separately below, because a - // hand-written tombstone is now what carries it.) - // @ts-expect-error objectui#6373 — the still-tombstoned members are refused by the tombstones alone. + // member still refused by the DERIVED tombstone is `label` alone — + // enough to keep this spread an error, and nothing but the tombstone + // refuses it. (`decimals` and `referenceTo` used to widen this list too; + // objectui#6625 and objectui#6597 each retired the `FieldMeta` member, so + // neither is spanned by this `Omit` at all any more — `FieldMeta` itself + // no longer declares them. Their own refusals are pinned separately below, + // because hand-written tombstones are now what carry them.) + // @ts-expect-error objectui#6373 — the still-tombstoned member is refused by the tombstone alone. const retiredRefused: EnrichedColumn = { header: 'h', accessorKey: 'a', ...({} as Omit) }; expect(retiredRefused.accessorKey).toBe('a'); }); @@ -337,6 +359,17 @@ describe("the emit type can FAIL — otherwise the annotation is decoration (#63 // @ts-expect-error objectui#6373/#6425/#6625 — `decimals` refused by the explicit retired-key tombstone. const writtenRefused: EnrichedColumn = carriesDecimals; expect(writtenRefused.accessorKey).toBe('a'); + + // objectui#6597's sibling pin, same shape: `referenceTo` was HELD (via + // `ObjectDataTableColumnHolds`) at the time this file's `decimals` pin was + // written, so it could not carry a tombstone yet. It measured no + // authoring story and retired the `FieldMeta` member too, so it now needs + // the identical hand-written-source treatment `decimals` gets above. + const carriesReferenceTo: { header: string; accessorKey: string; referenceTo?: unknown } = + { header: 'h', accessorKey: 'a', referenceTo: 'account' }; + // @ts-expect-error objectui#6373/#6597 — `referenceTo` refused by the explicit retired-key tombstone. + const referenceToWrittenRefused: EnrichedColumn = carriesReferenceTo; + expect(referenceToWrittenRefused.accessorKey).toBe('a'); }); it('the TOMBSTONE is what refuses the retired `decimals` at this emit', () => { @@ -352,4 +385,19 @@ describe("the emit type can FAIL — otherwise the annotation is decoration (#63 const untombstoned: Omit = carriesDecimals; expect(untombstoned.accessorKey).toBe('a'); }); + + it('the TOMBSTONE is what refuses the retired `referenceTo` at this emit', () => { + // ⭐ objectui#6597's counter-control, built the same way as `decimals`'s + // above. `EnrichedColumn` minus the retired-key tombstone and nothing else + // ACCEPTS the very source the directive refuses — so the refusal is the + // tombstone's, not the derived band's (which no longer reaches + // `referenceTo`, since the key left `keyof FieldMeta`), not the + // excess-property check's (the source is not fresh), and not weak-type + // detection's (`header` / `accessorKey` are in common, and both are + // required here). + const carriesReferenceTo: { header: string; accessorKey: string; referenceTo?: unknown } = + { header: 'h', accessorKey: 'a', referenceTo: 'account' }; + const untombstoned: Omit = carriesReferenceTo; + expect(untombstoned.accessorKey).toBe('a'); + }); }); diff --git a/packages/plugin-dashboard/src/__tests__/ObjectDataTable.overrideSource-6425.test.tsx b/packages/plugin-dashboard/src/__tests__/ObjectDataTable.overrideSource-6425.test.tsx index 95f571e41e..ac776aad87 100644 --- a/packages/plugin-dashboard/src/__tests__/ObjectDataTable.overrideSource-6425.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/ObjectDataTable.overrideSource-6425.test.tsx @@ -30,9 +30,13 @@ * immediately (the authored read is gone; the key is refused — since * objectui#6625 by an explicit tombstone rather than by the derived band, * because that card retired the `FieldMeta` member the band derived it from); - * `referenceTo` ⛔ NOT declared as spelled — still HELD, owned by - * objectui#6597. The measurements below are unchanged because the ruling did - * not change behaviour; what changed is which artefact answers for each key. + * `referenceTo` routed to objectui#6597 (⛔ not declared as spelled, and not + * decided here — a HELD key needed a demand MEASUREMENT the per-key ruling + * itself could not make). objectui#6597 measured no authoring story and + * RETIRED it too, the same mechanism as `decimals`: refused by an explicit + * tombstone since the key left `keyof FieldMeta`. The measurements below are + * unchanged because neither ruling changed behaviour; what changed is which + * artefact answers for each key. * * ## Every zero here is paired with a positive control * @@ -179,6 +183,14 @@ describe('per-key liveness of the five undeclared overrides (#6425)', () => { // — never `referenceTo` — and `computeLookupExpand` builds `$expand` from // the OBJECT SCHEMA's field types, so authoring it on a column does not add // the field to the expand whitelist either. Values arrive already expanded. + // + // This measurement is what objectui#6597 acted on: `AuthoredColumnOverrides` + // no longer even reads `referenceTo` off the authored column (`enrich()`'s + // `overrides:` object omits it), so the assertion below is now pinning "the + // withdraw verdict changed nothing" the same way the `decimals` test above + // pins its own retirement. The literal below still types as `any[]` at the + // `renderPair` call boundary, so authoring the retired spelling on a column + // is silently ignored at run time — exactly as before the retirement. const expanded = { id: 'acc-1', name: 'Acme' }; const { a, b } = await renderPair( { a: { type: 'lookup' }, b: { type: 'lookup' } }, @@ -216,6 +228,11 @@ describe('the override reads are typed, and the band can FAIL (#6425)', () => { // key is carried by the explicit tombstone now, so this stays a real // measurement instead of becoming TS2339 on a key that stopped existing. const heldDecimalsIsAny: IsAny = false; + // Same shape, objectui#6597: `referenceTo` moved from HELD (`unknown`, via + // `ObjectDataTableColumnHolds`) to RETIRED (`never`, via the explicit + // tombstone) without ever becoming unindexable — both answers are `false` + // here, so this line alone cannot tell HELD from RETIRED. The literal test + // below is what tells them apart. const heldReferenceToIsAny: IsAny = false; expect([bagCurrencyIsAny, bagDecimalsIsAny]).toEqual([true, true]); @@ -224,15 +241,14 @@ describe('the override reads are typed, and the band can FAIL (#6425)', () => { it('accepts exactly the adjudicated set', () => { // The positive control. Without it the refusals below could be satisfied by - // a type that refuses everything, which would pin nothing. `decimals` is - // deliberately NOT here any more: the ruling retired it, and its refusal - // is pinned with the band below. + // a type that refuses everything, which would pin nothing. `decimals` and + // `referenceTo` are deliberately NOT here any more: both rulings retired + // them, and their refusals are pinned with the band below. const accepted: AuthoredColumnOverrides = { accessorKey: 'amount', type: 'currency', format: '$0,0', options: [{ value: 'tech', label: 'Technology' }], - referenceTo: 'account', currency: 'EUR', }; expect(accepted.currency).toBe('EUR'); @@ -272,6 +288,21 @@ describe('the override reads are typed, and the band can FAIL (#6425)', () => { // @ts-expect-error objectui#6425/#6625 — `decimals` refused by the explicit retired-key tombstone. const decimalsRefused: AuthoredColumnOverrides = carriesDecimals; expect(decimalsRefused.accessorKey).toBe('amount'); + + // `referenceTo` used to be HELD here (`ObjectDataTableColumnHolds`); the + // ruling this file is named for routed the DEMAND question to objectui#6597 + // rather than deciding it. That card measured no authoring story and + // RETIRED the `FieldMeta` member itself, so — same mechanism as `decimals` + // above — the key left the band's POOL and its refusal is carried by + // `ObjectDataTableRetiredReferenceToTombstone` instead. Without that + // tombstone this directive would have turned TS2578-unused (or, before the + // ruling, would not have been an error at all — the key was HELD as + // `unknown`). The counter-control test below proves the tombstone, not the + // former hold, is what refuses it now. + const carriesReferenceTo: { accessorKey: string; referenceTo?: unknown } = { accessorKey: 'amount' }; + // @ts-expect-error objectui#6425/#6597 — `referenceTo` refused by the explicit retired-key tombstone. + const referenceToRefused: AuthoredColumnOverrides = carriesReferenceTo; + expect(referenceToRefused.accessorKey).toBe('amount'); }); it('the TOMBSTONE is what refuses `decimals` — not the band, not freshness', () => { @@ -294,6 +325,28 @@ describe('the override reads are typed, and the band can FAIL (#6425)', () => { expect(untombstoned.accessorKey).toBe('amount'); }); + it('the TOMBSTONE is what refuses `referenceTo` — not the band, not a lingering hold', () => { + // ⭐ objectui#6597's counter-control, built the same way as `decimals`'s + // above. `Omit<…, 'referenceTo'>` is `AuthoredColumnOverrides` minus the + // retired-key tombstone and nothing else, and it ACCEPTS the very source + // the directive above refuses. So the refusal comes from the tombstone: + // - not from `ObjectDataTableColumnHolds`, which is empty now — there is + // no lingering `referenceTo?: unknown` member for this source to match; + // - not from the DERIVED band, which cannot reach `referenceTo` any more — + // the key is no longer a `FieldMeta` member, so it is out of the pool + // `UnheldFieldMetaOverrideKey` excludes from; + // - not from the excess-property check, because the source is a VARIABLE, + // not a fresh literal; + // - not from weak-type detection, because `accessorKey` is in common. + // Delete the tombstone and this assignment still compiles while the + // directive above turns TS2578 — exactly the blindness objectui#6373's + // sibling tombstone was built to prevent, made observable here for the + // key this card retired. + const carriesReferenceTo: { accessorKey: string; referenceTo?: unknown } = { accessorKey: 'amount' }; + const untombstoned: Omit = carriesReferenceTo; + expect(untombstoned.accessorKey).toBe('amount'); + }); + it('the same source is ACCEPTED by the holds without the band', () => { // The band's counter-control: this is `AuthoredColumnOverrides` minus the // `?: never` members, and it takes the very source the directives above diff --git a/packages/plugin-dashboard/src/recordFields.tsx b/packages/plugin-dashboard/src/recordFields.tsx index 71975be30f..2f8dd34cf5 100644 --- a/packages/plugin-dashboard/src/recordFields.tsx +++ b/packages/plugin-dashboard/src/recordFields.tsx @@ -87,13 +87,34 @@ export const NUMERIC_FIELD_TYPES = new Set([ * key, which is what the retired write resolved to anyway and what * `NumberCellRenderer` already reads (`precision` is the total digit count, a * different question — objectui#2131). ⛔ Do not resurrect `decimals`. + * + * ⛔ `referenceTo` was RETIRED from this type by objectui#6597 (enforce-or-remove, + * withdraw branch — maintainer's standing startup-stage rule, 2026-08-27: no + * measured demand retires immediately). It was documented by this package's + * README as an author-facing column override, but the promise was never kept: + * `LookupCellRenderer` (`@object-ui/fields`) resolves its lookup target from + * `field.reference_to` / `field.reference` — never `field.referenceTo` — and + * `computeLookupExpand` (`ObjectDataTable.tsx`) builds `$expand` from the OBJECT + * SCHEMA's field types, never from an authored override. Measured with the + * `referenceTo`-vs-`options` positive control in + * `ObjectDataTable.overrideSource-6425.test.tsx`: `options` (a live override) + * separates two equal-valued columns, `referenceTo` does not — an authored + * override renders byte-identical to its absence. No authoring story survived + * the search either: the sibling `ObjectGrid` producer's own relational-meta + * pass-through (`applyRelationalMeta`, `plugin-grid/src/ObjectGrid.tsx`) copies + * `reference_to` / `reference` from the SCHEMA field def only, never from an + * authored column override, and no example/doc/fixture in this repo shows a + * table column pinning a lookup's target to something other than what its + * schema field already says. If a reader for the resolved reference target is + * ever wanted here, it reads `reference_to` / `reference` off the schema field + * def directly — the spelling `LookupCellRenderer` and `computeLookupExpand` + * actually use. ⛔ Do not resurrect `referenceTo`. */ export interface FieldMeta { name: string; label: string; type?: string; options?: Array<{ value: any; label: string; color?: string }>; - referenceTo?: unknown; format?: string; currency?: string; } @@ -132,30 +153,26 @@ export interface BuildFieldMetaParams { * member (objectui#6625): its last feeder went when objectui#6425's ruling * removed the authored read from `ObjectDataTable.enrich()`, and * `RecordDetailDrawer` — the only other caller — passes no overrides at all. + * `referenceTo` left the same way (objectui#6597): `ObjectDataTable.enrich()` + * no longer reads it off the authored column, and `RecordDetailDrawer` never + * did. */ overrides?: { type?: string; format?: string; options?: any; - referenceTo?: unknown; currency?: string; }; } /** - * Build the `FieldMeta` for a single field, resolving `referenceTo` and - * currency from the schema field def and translating select options. - * Column-level overrides win over schema-derived values. + * Build the `FieldMeta` for a single field, resolving currency from the + * schema field def and translating select options. Column-level overrides + * win over schema-derived values. */ export function buildFieldMeta(params: BuildFieldMetaParams): FieldMeta { const { accessorKey, label, def: meta, objectName, fieldOptionLabel, overrides = {} } = params; - const referenceTo = - overrides.referenceTo ?? - meta?.referenceTo ?? - (typeof meta?.reference === 'string' ? meta.reference : meta?.reference?.to) ?? - meta?.target; - let options: Array<{ value: any; label: string; color?: string }> | undefined = overrides.options ?? meta?.options; @@ -177,13 +194,19 @@ export function buildFieldMeta(params: BuildFieldMetaParams): FieldMeta { label, type: overrides.type ?? meta?.type, options, - referenceTo, format: overrides.format ?? meta?.format, currency: overrides.currency ?? meta?.currency ?? meta?.defaultCurrency, // ⛔ No `decimals` — RETIRED by objectui#6625. It resolved // `meta?.decimals ?? meta?.scale` on every call and reached no reader; the // `overrides.decimals ??` head of that chain had already lost its only // feeder to objectui#6425's ruling. A future reader reads `scale`. + // + // ⛔ No `referenceTo` — RETIRED by objectui#6597 (enforce-or-remove, + // withdraw). It resolved `overrides.referenceTo ?? meta?.referenceTo ?? + // meta?.reference(.to) ?? meta?.target` on every call and reached no + // reader: `LookupCellRenderer` resolves its target from + // `reference_to` / `reference`, never this spelling. A future reader + // reads `reference_to` / `reference` off the schema field def. }; } From 3c5d330a8190433b15a7b44098c7b89c08762ada Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 16:02:31 +0000 Subject: [PATCH 2/2] fix(plugin-dashboard): quiet no-empty-object-type on the now-empty column-holds interface `ObjectDataTableColumnHolds` lost its only member (`referenceTo`, retired by the previous commit) and eslint's `@typescript-eslint/no-empty-object-type` flags the resulting `{}`. Kept as an `interface` (a documented extension point for a future per-key ruling) rather than switched to `type = object`, with a scoped disable and the reasoning inline. Also adds the changeset for the referenceTo retirement. Claude-Session: https://claude.ai/code/session_8ca04858-ea8e-5b85-9182-de59aa49e00c Co-authored-by: Claude --- .../6597-retire-fieldmeta-referenceto.md | 61 +++++++++++++++++++ .../plugin-dashboard/src/ObjectDataTable.tsx | 8 +++ 2 files changed, 69 insertions(+) create mode 100644 .changeset/6597-retire-fieldmeta-referenceto.md diff --git a/.changeset/6597-retire-fieldmeta-referenceto.md b/.changeset/6597-retire-fieldmeta-referenceto.md new file mode 100644 index 0000000000..9e044dd2d3 --- /dev/null +++ b/.changeset/6597-retire-fieldmeta-referenceto.md @@ -0,0 +1,61 @@ +--- +'@object-ui/plugin-dashboard': minor +--- + +Retire `FieldMeta.referenceTo` and withdraw the README line documenting it as an +author-facing column override (objectui#6597, enforce-or-remove — withdraw branch). + +**The measurement.** The README documented `referenceTo` as an author override you could +pass on a `object-data-table` column to "bypass auto-detection" of a lookup's related +object. Two `keyof FieldMeta` populations exist, kept separate per the card's own trap +warning: `DatasetRelationship.referenceTo` (a resolver's *output*, unrelated) and this +package's `FieldMeta.referenceTo` (the card's actual subject). For the latter, neither the +schema-derived value nor an authored column override ever reached a reader: +`LookupCellRenderer` (`@object-ui/fields`) resolves its lookup target from +`field.reference_to` / `field.reference` — never `field.referenceTo` — and +`computeLookupExpand` builds `$expand` from the OBJECT SCHEMA's field types, never from an +authored column key. Re-measured on this branch's base (`881d5c292`) with the +`referenceTo`-vs-`options` positive control already in +`ObjectDataTable.overrideSource-6425.test.tsx`: `options` (a live override) separates two +equal-valued columns; `referenceTo` does not — an authored override renders +byte-identical to its absence. + +**No authoring story survived the search either.** `ObjectGrid`'s own relational-meta +pass-through (`applyRelationalMeta`, `plugin-grid/src/ObjectGrid.tsx`) copies +`reference_to` / `reference` / `display_field` / etc. from the SCHEMA field def only, at +all three of its call sites — never from an authored column override. No doc, example, or +fixture in this repo shows a table column pinning a lookup's target away from what its +schema field already says. Under the maintainer's standing startup-stage rule +(2026-08-27: deprecated/alias spellings retire immediately, no transition windows), no +measured demand selects withdraw. + +**Both `keyof FieldMeta` seam bands, both in scope.** `ObjectDataTable` derives two +refusal bands from `keyof FieldMeta` — `EnrichedColumn`'s write-side tombstones +(objectui#6373) and `AuthoredColumnOverrides`' read-side band (objectui#6425) — so +deleting the member would have dropped `referenceTo` from both as a side effect, silently +un-enforcing objectui#6425's "not declared as spelled, still HELD" verdict. A new +hand-written `ObjectDataTableRetiredReferenceToTombstone` (`{ referenceTo?: never }`) is +intersected into both halves of the seam, the exact sibling of +`ObjectDataTableRetiredDecimalsTombstone` (objectui#6625) — same mechanism, same reason. +`ObjectDataTableColumnHolds` — the interface that carried the HELD verdict — is now empty +(kept, not deleted, as the documented extension point a future ruling holds a new key +onto). + +**Ablation.** Removing the tombstone intersection (replacing it with the old +`{ referenceTo?: unknown }` HELD shape) turns two `@ts-expect-error` directives unused +(TS2578) — `tsc -p tsconfig.test.json` exits 2. Restoring the intersection returns a clean +exit 0. This proves the tombstone, not the derived band or a lingering hold, is what +refuses the key now. + +**Behaviour is unchanged** — pinned by the unchanged `referenceTo reaches NOTHING on this +path` runtime assertion, and by two new counter-control tests (mirroring the ones +objectui#6625 added for `decimals`) proving the tombstone specifically is what refuses the +key at both seam bands. + +Marked `minor` per this repo's version-alignment rule (AGENTS.md 版本号策略), which +reserves `major` for following `@objectstack` across a major. Scope note, measured rather +than assumed: `FieldMeta`, `AuthoredColumnOverrides`, `EnrichedColumn` and +`ObjectDataTableColumnHolds` are absent from `dist/index.d.ts` — `plugin-dashboard`'s +barrel re-exports only the `ObjectDataTable` component, and the package's `exports` map +publishes only `"."`. No downstream type moves; this is a package-internal contract change +plus a README correction, not a removal from a published type surface. diff --git a/packages/plugin-dashboard/src/ObjectDataTable.tsx b/packages/plugin-dashboard/src/ObjectDataTable.tsx index bf2a0190ea..77380736ae 100644 --- a/packages/plugin-dashboard/src/ObjectDataTable.tsx +++ b/packages/plugin-dashboard/src/ObjectDataTable.tsx @@ -324,6 +324,14 @@ export type EnrichedColumn = * role it played for `format` / `options` / `currency` before objectui#6425 * declared them and for `referenceTo` before objectui#6597 retired it. */ +// Deliberately empty: this is a documented EXTENSION POINT, not a stray +// placeholder. It is declared `interface` rather than `type = object` so a +// future per-key ruling can add a member the same way `format` / `options` / +// `currency` (before objectui#6425) and `referenceTo` (before objectui#6597) +// once did, without having to first convert it back from a type alias. +// `object` would accept the same intersections but reads as "no future member +// is expected here", which is the opposite of this type's role. +// eslint-disable-next-line @typescript-eslint/no-empty-object-type -- see above export interface ObjectDataTableColumnHolds {} /**