From c16891e016de9cae026b4e8be2c1db338e610a06 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 28 Aug 2026 07:03:48 +0000 Subject: [PATCH] refactor(plugin-dashboard): retire the written-never-read FieldMeta.decimals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `buildFieldMeta` resolved `decimals` from the schema def on every call and the value reached no reader. Re-measured on efdc6c62: zero `.decimals` member reads across @object-ui/fields, @object-ui/i18n, @object-ui/components, @object-ui/core and plugin-dashboard, with `.scale` as a positive control in the same query shape (NumberField.tsx, GridField.tsx, fields/src/index.tsx). The `overrides.decimals` half had already lost its only feeder to objectui#6425; `RecordDetailDrawer` passes no overrides at all. Both halves retire together. The refusal does NOT leave with the member. `ObjectDataTable` derives two bands from `keyof FieldMeta` — `EnrichedColumn`'s write-side tombstones and `AuthoredColumnOverrides`' read-side refusal — so deleting the member would have dropped `decimals` from both and silently un-enforced objectui#6425's retire, with nothing going red. `ObjectDataTableRetiredDecimalsTombstone` now carries that refusal by hand, intersected into both halves of the seam, the same shape as ObjectGrid's `ObjectGridRetiredOptionsTombstone`. Both pin suites keep their directives; each gains a counter-control proving the tombstone — not the derived band, not the excess-property check, not weak-type detection — is what refuses the key. The emit-side pin's source becomes a variable rather than a fresh literal for that reason. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49 --- .changeset/6625-retire-fieldmeta-decimals.md | 41 ++++++++++ .../plugin-dashboard/src/ObjectDataTable.tsx | 80 ++++++++++++++++--- ...ObjectDataTable.emitBoundary-6373.test.tsx | 58 +++++++++++--- ...jectDataTable.overrideSource-6425.test.tsx | 45 +++++++++-- .../plugin-dashboard/src/recordFields.tsx | 45 +++++++++-- 5 files changed, 235 insertions(+), 34 deletions(-) create mode 100644 .changeset/6625-retire-fieldmeta-decimals.md diff --git a/.changeset/6625-retire-fieldmeta-decimals.md b/.changeset/6625-retire-fieldmeta-decimals.md new file mode 100644 index 0000000000..6416d21ca2 --- /dev/null +++ b/.changeset/6625-retire-fieldmeta-decimals.md @@ -0,0 +1,41 @@ +--- +'@object-ui/plugin-dashboard': minor +--- + +Retire `FieldMeta.decimals` and the now-unfed `BuildFieldMetaParams.overrides.decimals` +from `plugin-dashboard`'s shared field-rendering helpers (objectui#6625, +enforce-or-remove). + +`buildFieldMeta` computed `decimals: overrides.decimals ?? meta?.decimals ?? meta?.scale` +on **every** call and the value **reached nothing**. Re-measured on this branch's base +(`efdc6c62`): **zero `.decimals` member reads** across `@object-ui/fields`, +`@object-ui/i18n`, `@object-ui/components`, `@object-ui/core` and `plugin-dashboard` +itself — the only non-comment occurrence was the write being removed here. The +**positive control in the same query shape** fires: `.scale` member reads hit +`NumberField.tsx`, `GridField.tsx` and `fields/src/index.tsx`. So the zero is a +finding, not a broken query. The `overrides.decimals ??` head of that chain had already +lost its only feeder when objectui#6425's ruling removed the authored read from +`ObjectDataTable.enrich()`; `RecordDetailDrawer`, the only other `buildFieldMeta` +caller, passes no overrides at all. Both halves retire together, so the key leaves in +one move. + +Behaviour is unchanged — no reader existed to notice, pinned by the unchanged runtime +assertion in `ObjectDataTable.overrideSource-6425.test.tsx` that an authored `decimals` +renders byte-identical to its absence. + +**The refusal did NOT leave with the member.** `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 `decimals` from both as a side effect, silently un-enforcing +objectui#6425's retire. A new hand-written `ObjectDataTableRetiredDecimalsTombstone` +(`{ decimals?: never }`) is intersected into both halves of the seam, the same shape and +for the same reason as `ObjectGrid`'s `ObjectGridRetiredOptionsTombstone`. The verdict +is unchanged since 2026-08-27; only the artefact enforcing it moved, from derived to +hand-written. + +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`, `BuildFieldMetaParams`, `AuthoredColumnOverrides` and +`EnrichedColumn` are **absent from `dist/index.d.ts`** — they are not re-exported by the +package barrel, and the `exports` map publishes only `"."`, so no consumer can name them. +No downstream type moves; this is a package-internal contract change. diff --git a/packages/plugin-dashboard/src/ObjectDataTable.tsx b/packages/plugin-dashboard/src/ObjectDataTable.tsx index e29ef08381..60ea37191d 100644 --- a/packages/plugin-dashboard/src/ObjectDataTable.tsx +++ b/packages/plugin-dashboard/src/ObjectDataTable.tsx @@ -131,6 +131,43 @@ interface NormalizedColumn { * seam instead of arriving anonymously inside a spread. When #5120 retires the * consumer alias, this member becomes a tombstone with it. */ +/** + * ⭐ `decimals` — RETIRED from `FieldMeta` ITSELF (objectui#6625), and this + * hand-written tombstone is now the ONLY enforcement of that key's refusal at + * BOTH halves of this seam. ⛔ Do not "tidy" it away as redundant. + * + * Until objectui#6625 the key needed no tombstone: it was a `FieldMeta` member, + * so BOTH bands below — {@link EnrichedColumn}'s write-side tombstones + * (objectui#6373) and {@link UnheldFieldMetaOverrideKey}'s read-side refusal + * (objectui#6425) — derived it for free. Refusal by MEMBERSHIP. #6625 then + * retired the member (written from the schema def, read by nothing), which + * removed it from `keyof FieldMeta` and silently ended that enforcement at both + * ends. Nothing would have gone red at the moment of loss: on the read side the + * suite's `@ts-expect-error` would merely have turned TS2578-unused, and on the + * write side the hand-written pin would have gone on passing on the + * excess-property check alone — a pin passing because its subject stopped + * existing. + * + * This is the exact mirror of the rule `ObjectGrid`'s sibling tombstone + * records (`ObjectGridRetiredOptionsTombstone`, objectui#6425): *a pin enforced + * by a key's non-membership silently stops enforcing the moment the key becomes + * a member.* Read from this end: **a refusal DERIVED from a key's membership + * silently stops enforcing the moment that member is deleted.** Same seam, same + * blindness, opposite direction — which is why the retirement had to be + * re-stated by hand rather than inherited. + * + * objectui#6425's verdict is unchanged — no reader for an authored `decimals` + * existed then and none exists now — and so is the runtime behaviour: the value + * the retired `buildFieldMeta` write resolved reached nothing. Only the + * refusal's mechanism moves, from derived to hand-written. It is intersected + * into BOTH types below, because the retirement belongs to the SEAM, not to one + * of its two halves — the same reason `ObjectGrid` intersects its own tombstone + * into both its draft and its post-fold column. ⭐ A future reader for decimal + * places reads `scale`, never this key (objectui#6625; `NumberCellRenderer` + * already does). + */ +type ObjectDataTableRetiredDecimalsTombstone = { decimals?: never }; + export type EnrichedColumn = TableColumn /** HELD alias, objectui#5120 — see above. Not declared by `TableColumn`. */ @@ -138,7 +175,8 @@ export type EnrichedColumn = /** RETIRED at this emit seam, objectui#6373 — derived, never hand-listed, so * a future `FieldMeta` member is tombstoned by default and has to be * adjudicated to escape. */ - & { [K in Exclude]?: never }; + & { [K in Exclude]?: never } + & ObjectDataTableRetiredDecimalsTombstone; /** * What this widget's column producer is allowed to READ off the AUTHORED @@ -167,8 +205,10 @@ export type EnrichedColumn = * - `currency` — DECLARED (kept in production, never promised before; * declaring makes the existing behaviour honest). Same `Pick`. * - `decimals` — RETIRED, immediately (neither promised nor kept: zero - * readers measured, and the authored read below is gone). The derived - * band now refuses it like any other unadjudicated `FieldMeta` member. + * readers measured, and the authored read below is gone). Refused by + * {@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). @@ -182,8 +222,12 @@ export type EnrichedColumn = * `FieldMeta` is the override vocabulary — `buildFieldMeta`'s `overrides` is a * subset of it — so it is the pool a new tolerance would come from, and the * same pool {@link EnrichedColumn}'s tombstones derive from. Deriving means a - * seventh `FieldMeta` member is refused here on the day it is added, without - * anyone remembering to extend a hand-written list. + * 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.) * * ⚠️ It derives from the OVERRIDE VOCABULARY, not from the authored input * type, and that difference is forced rather than stylistic. `plugin-grid`'s @@ -230,7 +274,9 @@ export type EnrichedColumn = * left: 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 (the derived band refuses it now). + * outright — refused by {@link ObjectDataTableRetiredDecimalsTombstone} since + * objectui#6625 retired the `FieldMeta` member that used to carry it into the + * derived band. */ export interface ObjectDataTableColumnHolds { /** @@ -253,9 +299,20 @@ export interface ObjectDataTableColumnHolds { * {@link ObjectDataTableColumnHolds} to escape. Keys `TableColumn` declares * leave the pool by declaration: `type` (objectui#5853 owns its VALUE set, * folded below by `normalizeTableColumnType`) and, since objectui#6425's - * ruling, `format` / `options` / `currency`. `decimals` is the member the - * same ruling RETIRED — it lands here, refused at the read site, which is - * exactly the "held-band verdict flips to RETIRED" the ruling asked for. + * 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. + * + * 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 + * has (see the docblock above). */ export type UnheldFieldMetaOverrideKey = Exclude; @@ -271,7 +328,10 @@ export type AuthoredColumnOverrides = * their published types, straight off the declaration. */ & Pick & ObjectDataTableColumnHolds - & { [K in UnheldFieldMetaOverrideKey]?: never }; + & { [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; /** * Shared empty fallback for the resolved row list (objectui#4629). 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 91265cbf86..32129b7768 100644 --- a/packages/plugin-dashboard/src/__tests__/ObjectDataTable.emitBoundary-6373.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/ObjectDataTable.emitBoundary-6373.test.tsx @@ -210,10 +210,14 @@ describe('ObjectDataTable emits only what the columns slot declares (#6373)', () it('names the six keys that retired, and keeps the one that is held', async () => { const cols = await emit({ type: 'object-data-table', objectName: 'account' }); for (const col of cols) { - // `Object.keys`, not a value read: `buildFieldMeta` always returns all - // eight members, 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. + // `Object.keys`, not a value read: `buildFieldMeta` returns all of its + // 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.) for (const key of RETIRED) { expect(Object.keys(col), `${col.accessorKey}.${key}`).not.toContain(key); } @@ -296,12 +300,15 @@ describe("the emit type can FAIL — otherwise the annotation is decoration (#63 // — so on its own it pins "the spread is refused" without pinning WHY, and // would have gone on passing after the enforcement was removed. // - // `Omit` spans the retired six: `name` is + // `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 tombstones are `label`, `referenceTo` and - // `decimals` — enough to keep this spread an error, and nothing but the - // tombstones refuses it. + // 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. const retiredRefused: EnrichedColumn = { header: 'h', accessorKey: 'a', ...({} as Omit) }; expect(retiredRefused.accessorKey).toBe('a'); @@ -313,9 +320,36 @@ describe("the emit type can FAIL — otherwise the annotation is decoration (#63 // by different machinery. This pin carried `format` until the #6425 // ruling declared it (writing it by hand is now legal, see the accepted // fixture above); `decimals` — the key the same ruling RETIRED — takes - // its place, refused by the derived tombstone. - // @ts-expect-error objectui#6373/#6425 — `decimals` retired from this emit seam. - const writtenRefused: EnrichedColumn = { header: 'h', accessorKey: 'a', decimals: 2 }; + // its place. + // + // ⚠️ The source is a VARIABLE, not a fresh object literal, and that is + // objectui#6625's doing rather than style. A fresh literal is refused by + // the excess-property check whether or not any tombstone exists, so as a + // literal this pinned "something refused this" without pinning what — and + // it would have gone on passing after #6625 removed `decimals` from + // `FieldMeta` and thus from the DERIVED tombstone band, which is a pin + // passing because its subject stopped existing. A non-fresh source skips + // the freshness check and reaches + // `ObjectDataTableRetiredDecimalsTombstone` and nothing else. The control + // below proves that is what answers. + const carriesDecimals: { header: string; accessorKey: string; decimals?: number } = + { header: 'h', accessorKey: 'a', decimals: 2 }; + // @ts-expect-error objectui#6373/#6425/#6625 — `decimals` refused by the explicit retired-key tombstone. + const writtenRefused: EnrichedColumn = carriesDecimals; expect(writtenRefused.accessorKey).toBe('a'); }); + + it('the TOMBSTONE is what refuses the retired `decimals` at this emit', () => { + // ⭐ objectui#6625's counter-control for the pin 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 `decimals`, 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 carriesDecimals: { header: string; accessorKey: string; decimals?: number } = + { header: 'h', accessorKey: 'a', decimals: 2 }; + const untombstoned: Omit = carriesDecimals; + 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 0d54df8324..95f571e41e 100644 --- a/packages/plugin-dashboard/src/__tests__/ObjectDataTable.overrideSource-6425.test.tsx +++ b/packages/plugin-dashboard/src/__tests__/ObjectDataTable.overrideSource-6425.test.tsx @@ -27,7 +27,9 @@ * anything at all, so the ruling could be made on evidence. The maintainer * ruled per key on 2026-08-27 (objectui#6425): `format` / `options` / * `currency` DECLARED on `TableColumn` + its zod mirror; `decimals` RETIRED - * immediately (the authored read is gone; the derived band refuses the key); + * 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. @@ -137,6 +139,11 @@ describe('per-key liveness of the five undeclared overrides (#6425)', () => { // key — and this same pin now proves the retire itself changed nothing: // an authored `decimals` renders byte-identical to its absence, before // the read was removed and after. + // + // objectui#6625 extends what this same unchanged assertion covers: the + // `FieldMeta` MEMBER and `buildFieldMeta`'s schema-derived write are gone + // too, so this now also measures that removing the write changed no render + // — which is the whole behaviour claim of that card. const { a, b } = await renderPair( { a: { type: 'number' }, b: { type: 'number' } }, { a: 3.14159, b: 3.14159 }, @@ -205,6 +212,9 @@ describe('the override reads are typed, and the band can FAIL (#6425)', () => { const bagCurrencyIsAny: IsAny = true; const bagDecimalsIsAny: IsAny = true; const heldCurrencyIsAny: IsAny = false; + // Still INDEXABLE after objectui#6625 retired the `FieldMeta` member: the + // 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; const heldReferenceToIsAny: IsAny = false; @@ -250,15 +260,40 @@ describe('the override reads are typed, and the band can FAIL (#6425)', () => { const nameRefused: AuthoredColumnOverrides = carriesName; expect(nameRefused.accessorKey).toBe('amount'); - // `decimals` used to be HELD here; the ruling (2026-08-27) RETIRED it, so - // it fell into the derived band with no hand-edit to the band itself — - // removing the hold member IS the flip. This directive is the pin. + // `decimals` used to be HELD here; the ruling (2026-08-27) RETIRED it into + // the derived band. objectui#6625 then retired the `FieldMeta` MEMBER the + // band derived it from, so the key left the band's POOL — and the refusal + // is carried by `ObjectDataTableRetiredDecimalsTombstone` instead. ⚠️ The + // verdict and this directive are BOTH unchanged; only the artefact that + // enforces them moved. Without that tombstone this directive would have + // turned TS2578-unused — the pin going green because its subject stopped + // existing. The test below is the control that proves it did not. const carriesDecimals: { accessorKey: string; decimals?: number } = { accessorKey: 'amount' }; - // @ts-expect-error objectui#6425 — `decimals` RETIRED into the derived refusal band. + // @ts-expect-error objectui#6425/#6625 — `decimals` refused by the explicit retired-key tombstone. const decimalsRefused: AuthoredColumnOverrides = carriesDecimals; expect(decimalsRefused.accessorKey).toBe('amount'); }); + it('the TOMBSTONE is what refuses `decimals` — not the band, not freshness', () => { + // ⭐ objectui#6625's counter-control, built the same way as the band's + // control below. `Omit<…, 'decimals'>` 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 the DERIVED band, which cannot reach `decimals` 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 — which is exactly the blindness this card + // existed to prevent, made observable in one file. + const carriesDecimals: { accessorKey: string; decimals?: number } = { accessorKey: 'amount' }; + const untombstoned: Omit = carriesDecimals; + 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 a0e454a7d5..71975be30f 100644 --- a/packages/plugin-dashboard/src/recordFields.tsx +++ b/packages/plugin-dashboard/src/recordFields.tsx @@ -65,6 +65,29 @@ export const NUMERIC_FIELD_TYPES = new Set([ 'currency', 'money', 'number', 'integer', 'decimal', 'float', 'percent', 'percentage', ]); +/** + * The override vocabulary this package's two field surfaces share. + * + * ⚠️ This type is a DERIVATION SOURCE, not only a shape: `ObjectDataTable`'s + * two column bands are both `Exclude` — `EnrichedColumn`'s + * write-side tombstones (objectui#6373) and `AuthoredColumnOverrides`' read-side + * refusal band (objectui#6425). So a member REMOVED here silently leaves both + * bands, and any refusal that rode on its membership stops enforcing with + * nothing going red. A member being retired must therefore be re-refused + * explicitly at that seam before it leaves this type — see + * `ObjectDataTableRetiredDecimalsTombstone` in `ObjectDataTable.tsx`, which is + * what `decimals` left behind. + * + * ⛔ `decimals` was RETIRED from this type by objectui#6625 — written from the + * schema def on every call and read by nothing (zero `.decimals` member reads + * across `@object-ui/fields`, `@object-ui/i18n`, `@object-ui/components`, + * `@object-ui/core` and this package, measured against a `.scale` positive + * control that hits `NumberField.tsx` / `GridField.tsx` / `index.tsx`). If a + * reader is ever wanted here it reads `scale` — the field def's decimal-places + * 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`. + */ export interface FieldMeta { name: string; label: string; @@ -73,7 +96,6 @@ export interface FieldMeta { referenceTo?: unknown; format?: string; currency?: string; - decimals?: number; } /** @@ -102,20 +124,27 @@ export interface BuildFieldMetaParams { objectName?: string; /** Translator for per-option labels (from `useSafeFieldLabel`). */ fieldOptionLabel?: (objectName: string, field: string, value: string, fallback: string) => string; - /** Per-column overrides (table columns may pin type/format/options). */ + /** + * Per-column overrides (table columns may pin type/format/options). + * + * A subset of {@link FieldMeta}, which is what makes that type the pool + * `ObjectDataTable`'s refusal band derives from. `decimals` left with the + * 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. + */ overrides?: { type?: string; format?: string; options?: any; referenceTo?: unknown; currency?: string; - decimals?: number; }; } /** - * Build the `FieldMeta` for a single field, resolving `referenceTo`, currency - * and decimals from the schema field def and translating select options. + * 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. */ export function buildFieldMeta(params: BuildFieldMetaParams): FieldMeta { @@ -151,8 +180,10 @@ export function buildFieldMeta(params: BuildFieldMetaParams): FieldMeta { referenceTo, format: overrides.format ?? meta?.format, currency: overrides.currency ?? meta?.currency ?? meta?.defaultCurrency, - // `scale` (decimal places), not `precision` (total digit count) — see #2131. - decimals: overrides.decimals ?? meta?.decimals ?? meta?.scale, + // ⛔ 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`. }; }