diff --git a/.changeset/6152-resizable-columns-retired.md b/.changeset/6152-resizable-columns-retired.md new file mode 100644 index 0000000000..78a424b61b --- /dev/null +++ b/.changeset/6152-resizable-columns-retired.md @@ -0,0 +1,27 @@ +--- +'@object-ui/types': minor +'@object-ui/plugin-grid': minor +--- + +feat(types)!: retire `object-grid`'s `resizableColumns` on both faces, and stop `ObjectGrid` reading it (objectui#6152, round 7) + +**Retired (breaking).** `resizableColumns` was the legacy second spelling of `resizable` on +`object-grid`, read only when `resizable` was absent. `@objectstack/spec` 17.7.0 retired it in the +`object-grid` row (objectstack#21445), so an authored document that writes it, flat on the node or +inside its `properties` bag, was already refused by name before this change. This change retires +the rest of it, with no alias window: + +- `@object-ui/types`: `ObjectGridSchema.resizableColumns` is now `?: never`, so writing it on a + typed literal is a `tsc` error. The flat zod `ObjectGridSchema` in `@object-ui/types/zod` (the + node as `ObjectGrid` reads it after the `properties` hoist, or as code composes it) refuses the + key by name at the key, where `.passthrough()` used to keep it unexamined. The refusal names + `properties.resizable`. +- `@object-ui/plugin-grid`: `ObjectGrid` reads `resizable` alone. A node that still carries + `resizableColumns` (one composed in code, past the type) now gets the default, resizable + columns, as if it had written nothing: `resizableColumns: false` no longer turns the column + resize handles off. The `object-grid` registration's `resizable` input no longer describes the + old spelling as a fallback. + +Rename the key to `resizable`; on an authored node, that is `properties.resizable`. + +An `object-view`'s `table.resizableColumns` is unchanged: the slot already refused it by name. diff --git a/packages/plugin-grid/src/ObjectGrid.tsx b/packages/plugin-grid/src/ObjectGrid.tsx index 63df3a1d89..2ec85adbb4 100644 --- a/packages/plugin-grid/src/ObjectGrid.tsx +++ b/packages/plugin-grid/src/ObjectGrid.tsx @@ -5431,7 +5431,11 @@ export const ObjectGrid: React.FC = ({ // `editable` produces no such column today. Following the same verdict is // what makes the gated grid identical to the non-editable one. rowActions: !!(inlineEditable && hasActions), - resizableColumns: schema.resizable ?? schema.resizableColumns ?? true, + // `resizable` alone (objectui#6152 round 7): the legacy `resizableColumns` + // spelling is retired on both faces and by `@objectstack/spec` 17.7.0, so + // it is no longer read here. (`resizableColumns` on the left is DataTable's + // own prop, a different block's key.) + resizableColumns: schema.resizable ?? true, reorderableColumns: schema.reorderableColumns ?? false, // [#5143] The authored key ∧ this principal's write verdict on the object. editable: inlineEditable, diff --git a/packages/plugin-grid/src/__tests__/ObjectGrid.resizableColumnsRetired-6152.test.tsx b/packages/plugin-grid/src/__tests__/ObjectGrid.resizableColumnsRetired-6152.test.tsx new file mode 100644 index 0000000000..6db21b1bcb --- /dev/null +++ b/packages/plugin-grid/src/__tests__/ObjectGrid.resizableColumnsRetired-6152.test.tsx @@ -0,0 +1,80 @@ +/** + * objectui#6152 round 7 — `ObjectGrid` reads `resizable` alone. + * + * `resizableColumns` was the legacy second spelling of `resizable`, read only when + * `resizable` was absent (`schema.resizable ?? schema.resizableColumns ?? true`). It is + * retired on both faces of `@object-ui/types` and by `@objectstack/spec` 17.7.0, which + * refuses it in the `object-grid` row, so the fallback read went in the same change: + * a node that still carries the old spelling (one composed in code, past the type) + * gets the default, resizable columns, exactly as if it had written nothing. + * + * Observed in the DOM: each resizable header cell draws one `.cursor-col-resize` drag + * handle (`data-table`'s own markup, the one `columnStatePersistence.test.tsx` drags). + */ +import { describe, it, expect, beforeEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import React from 'react'; +import type { ObjectGridSchema } from '@object-ui/types'; + +import { ObjectGrid } from '../ObjectGrid'; +import { __clearRecordCrudVerdictCache } from '../hooks/useRecordCrudVerdicts'; +import { installExplainDouble } from './explainDouble'; +import { registerAllFields } from '@object-ui/fields'; +import { ActionProvider } from '@object-ui/react'; + +registerAllFields(); + +beforeEach(() => { + __clearRecordCrudVerdictCache(); + installExplainDouble(); + localStorage.clear(); +}); + +const rows = [ + { id: '1', name: 'Alice', amount: 100 }, + { id: '2', name: 'Bob', amount: 200 }, +]; + +/** Render a two-column grid and count the header resize handles it draws. */ +async function resizeHandles(extra: Record): Promise { + // Cast through `unknown`: the retired spelling is a `tsc` error on `ObjectGridSchema` + // now, and this writes it on purpose, as a node composed past the type would. + const schema = { + type: 'object-grid', + objectName: 'test_object', + columns: [ + { field: 'name', label: 'Name' }, + { field: 'amount', label: 'Amount', type: 'number' }, + ], + data: { provider: 'value', items: rows }, + ...extra, + } as unknown as ObjectGridSchema; + const { container, unmount } = render( + + + , + ); + await waitFor(() => expect(screen.getByText('Alice')).toBeInTheDocument()); + const count = container.querySelectorAll('th .cursor-col-resize').length; + unmount(); + return count; +} + +describe('objectui#6152 round 7 — `ObjectGrid` no longer reads `resizableColumns`', () => { + it('CONTROL: with neither key, both columns draw a resize handle (on by default)', async () => { + expect(await resizeHandles({})).toBe(2); + }); + + it('LIT CONTROL: `resizable: false` turns the handles off, so the count can move', async () => { + expect(await resizeHandles({ resizable: false })).toBe(0); + }); + + it('`resizableColumns: false` alone is not read: the grid keeps its default handles', async () => { + expect(await resizeHandles({ resizableColumns: false })).toBe(2); + }); + + it('`resizable: false` still wins when the retired spelling says otherwise', async () => { + expect(await resizeHandles({ resizable: false, resizableColumns: true })).toBe(0); + }); +}); diff --git a/packages/plugin-grid/src/index.tsx b/packages/plugin-grid/src/index.tsx index 103b678daf..1c4fa26d7d 100644 --- a/packages/plugin-grid/src/index.tsx +++ b/packages/plugin-grid/src/index.tsx @@ -211,6 +211,9 @@ export const ObjectGridRenderer: React.FC<{ schema: any; [key: string]: any }> = * turned it into a retired-key tombstone the protocol refuses by name, and * objectui#5861 removed every renderer read of it (ADR-0049 enforce-or-remove). * It stays off this list because the contract refuses it, not by exemption. + * `resizableColumns` followed the same way: spec 17.7.0 tombstoned it + * (objectstack#21445), and objectui#6152 round 7 removed `ObjectGrid`'s + * `resizable ?? resizableColumns` read, so `resizable` is its only spelling. * * ## `description`, `emptyState` and `keyboardNavigation`: the 17.6.0 keys * @@ -298,7 +301,7 @@ const GRID_QUERY_INPUTS: ComponentInput[] = [ // ── presentation ────────────────────────────────────────────────────────── { name: 'rowHeight', type: 'enum', enum: ['compact', 'short', 'medium', 'tall', 'extra_tall'], description: 'Row density. An unrecognised value falls back to `compact` rather than erroring.' }, { name: 'frozenColumns', type: 'number', description: 'How many leading columns stay pinned while the grid scrolls horizontally.' }, - { name: 'resizable', type: 'boolean', description: 'Let users drag column borders to resize. The canonical spelling — the deprecated `resizableColumns` is only read when this is absent.' }, + { name: 'resizable', type: 'boolean', description: 'Let users drag column borders to resize. On by default. The only spelling — the legacy `resizableColumns` is retired and refused by name (objectui#6152).' }, { name: 'reorderableColumns', type: 'boolean', description: 'Let users drag columns into a different order.' }, { name: 'showColumnTypeIcons', type: 'boolean', description: 'Show a field-type icon in each column header. Off by default — the type is usually obvious from the cell content, and the icons compete with the column labels.' }, { name: 'rowColor', type: 'object', description: 'Rules that colour whole rows from a field value.' }, diff --git a/packages/types/src/__tests__/object-view-slot-key-lists.test.ts b/packages/types/src/__tests__/object-view-slot-key-lists.test.ts index f84064075e..0299d8ad86 100644 --- a/packages/types/src/__tests__/object-view-slot-key-lists.test.ts +++ b/packages/types/src/__tests__/object-view-slot-key-lists.test.ts @@ -152,7 +152,12 @@ const TABLE_WITHHELD_BY_REASON = { * of these to it. */ nodeLevel: ['ariaLabel', 'disabled', 'disabledOn', 'hidden', 'hiddenOn', 'responsiveStyles', 'style', 'testId', 'visible', 'visibleOn', 'visibleWhen'], - /** The legacy alias of a relayed key: `bulkActions`, `resizable`. */ + /** + * The legacy alias of a relayed key: `bulkActions`, `resizable`. `resizableColumns` + * is also a retirement tombstone on `ObjectGridSchema` itself since objectui#6152 + * round 7 (`ObjectGrid` no longer reads it); the slot still refuses it with its own + * message, as it does the `unread` five. + */ alias: ['batchActions', 'resizableColumns'], } as const; @@ -163,7 +168,8 @@ const TABLE_WITHHELD_KEYS: readonly string[] = Object.values(TABLE_WITHHELD_BY_R * faces) that the slot keeps: they type nothing, and they carry the named * refusal and its guidance onto this face too. The five the grid retired in * objectui#11068 are in the withheld set above instead, refused by the slot's - * own message. + * own message, and so is `resizableColumns`, which the grid retired in + * objectui#6152 round 7. */ const TABLE_INHERITED_TOMBSTONES = ['body', 'children', 'defaultSort'] as const; diff --git a/packages/types/src/__tests__/unmirrored-declared-round6-6152.test.ts b/packages/types/src/__tests__/unmirrored-declared-round6-6152.test.ts index e47fc3c7f5..4f8ef0fcd0 100644 --- a/packages/types/src/__tests__/unmirrored-declared-round6-6152.test.ts +++ b/packages/types/src/__tests__/unmirrored-declared-round6-6152.test.ts @@ -23,8 +23,10 @@ * The authored `object-grid` node does not move: its bag is the spec row by * reference, and a prop written flat on the node stays refused by name. * - * `resizableColumns`, the eleventh key, is NOT mirrored. Its route is open on the - * card, and `zod-mirror-parity.test.ts` keeps it in `UnmirroredDeclared`. + * `resizableColumns`, the eleventh key, was NOT mirrored: its route was open on the + * card until round 7 RETIRED it. It is a member of the mirror since then, as a + * retirement tombstone that refuses every value, and `zod-mirror-parity.test.ts` + * no longer ledgers it (`unmirrored-declared-round7-6152.test.ts` pins the rest). */ import { describe, it, expect } from 'vitest'; @@ -92,8 +94,15 @@ describe('objectui#6152 round 6 — the flat `ObjectGridSchema` mirror judges te expect(refused.every((i) => i.path[0] === key), JSON.stringify(refused)).toBe(true); }); - it('`resizableColumns` is NOT mirrored: its route is open, and the parity ledger keeps it', () => { - expect('resizableColumns' in ObjectGridSchema.shape).toBe(false); + it('`resizableColumns` was not mirrored but RETIRED (round 7): a member that refuses every value', () => { + // Flipped by objectui#6152 round 7. This read `false` while the key's route was + // open; the retirement made it a tombstone member, which is what a passthrough + // mirror needs to refuse the key instead of keeping it unexamined. + expect('resizableColumns' in ObjectGridSchema.shape).toBe(true); + for (const value of [true, false]) { + expect(codeAndPath(ObjectGridSchema.safeParse({ ...GRID, resizableColumns: value }))) + .toEqual([{ code: 'invalid_type', path: ['resizableColumns'] }]); + } }); it('the members the twin takes from the spec by name are the spec\'s, by reference', () => { diff --git a/packages/types/src/__tests__/unmirrored-declared-round7-6152.test.ts b/packages/types/src/__tests__/unmirrored-declared-round7-6152.test.ts new file mode 100644 index 0000000000..eebc0b8a99 --- /dev/null +++ b/packages/types/src/__tests__/unmirrored-declared-round7-6152.test.ts @@ -0,0 +1,132 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * objectui#6152 round 7 — `ObjectGridSchema.resizableColumns` RETIRED on both faces. + * + * `resizable` is canonical, and `resizableColumns` (its legacy second spelling, read only + * when `resizable` was absent) retires with no window. Round 6 stopped at the key: the + * authored `object-grid` node's `properties` bag is the spec's + * `ComponentPropsMap['object-grid']` row by reference (objectui#11276), and that row still + * declared the alias, so a local retirement alone would have left the bag accepting a key + * nothing read. `@objectstack/spec` 17.7.0 (objectstack#21445) retired it in the row, and + * objectui resolves 17.7.0 since objectui#11717. So this round makes the four strokes: + * + * - `resizableColumns?: never` on the interface; + * - a `retirementTombstone()` on the flat zod mirror, naming `properties.resizable`; + * - `ObjectGrid`'s `resizable ?? resizableColumns` fallback read dropped (pinned in + * `@object-ui/plugin-grid`, `ObjectGrid.resizableColumnsRetired-6152.test.tsx`); + * - the key off `zod-mirror-parity.test.ts`'s `UnmirroredDeclared` (its entry went with + * it: a tombstone is a mirrored member). + * + * The protocol block below is the precondition, re-read against the INSTALLED spec: the + * local tombstone must never refuse what the installed protocol accepts. + */ +import { describe, it, expect } from 'vitest'; + +import { ObjectGridPropsSchema as SpecObjectGridPropsSchema } from '@objectstack/spec/ui'; +import type { ObjectGridSchema as TsObjectGridSchema } from '../objectql'; +import { ObjectGridSchema, ObjectViewSchema } from '../zod/objectql.zod.js'; +import { AnyComponentSchema, StrictAnyComponentSchema, safeValidateSchema } from '../zod/index.zod.js'; + +type Issue = { code: string; path: PropertyKey[]; message: string }; +type Parsed = { success: boolean; error?: { issues: readonly Issue[] } }; +type Parse = (doc: unknown) => Parsed; + +const codeAndPath = (r: Parsed) => (r.success ? [] : r.error!.issues.map((i) => ({ code: i.code, path: i.path }))); + +const GRID = { type: 'object-grid', objectName: 'task' }; +const AUTHORED = { type: 'object-grid', properties: { objectName: 'task' } }; +const VIEW = { type: 'object-view', objectName: 'task' }; + +const DOORS: ReadonlyArray = [ + ['the tolerant face', (d) => AnyComponentSchema.safeParse(d)], + ['the strict face', (d) => StrictAnyComponentSchema.safeParse(d)], + ['safeValidateSchema', (d) => safeValidateSchema(d)], +]; + +describe('objectui#6152 round 7 — the installed protocol refuses `resizableColumns` (the precondition)', () => { + it('the `object-grid` row refuses `resizableColumns` by name, and takes `resizable`', () => { + expect(codeAndPath(SpecObjectGridPropsSchema.safeParse({ resizableColumns: false }))) + .toEqual([{ code: 'invalid_type', path: ['resizableColumns'] }]); + // Lit control: the canonical spelling parses on the same row. + expect(SpecObjectGridPropsSchema.safeParse({ resizable: false }).success).toBe(true); + }); +}); + +describe('objectui#6152 round 7 — the flat `ObjectGridSchema` mirror refuses the retired spelling', () => { + it('CONTROL: the minimal node and the canonical `resizable` parse', () => { + expect(ObjectGridSchema.safeParse(GRID).success).toBe(true); + expect(ObjectGridSchema.safeParse({ ...GRID, resizable: false }).success).toBe(true); + }); + + it('`resizableColumns` is a MEMBER, so `.passthrough()` no longer keeps it unexamined', () => { + expect('resizableColumns' in ObjectGridSchema.shape).toBe(true); + }); + + it.each([true, false])('`resizableColumns: %s` is refused at the key, naming `properties.resizable`', (value) => { + const r = ObjectGridSchema.safeParse({ ...GRID, resizableColumns: value }); + expect(codeAndPath(r)).toEqual([{ code: 'invalid_type', path: ['resizableColumns'] }]); + expect(r.success ? '' : r.error.issues[0].message).toContain('properties.resizable'); + }); +}); + +describe('objectui#6152 round 7 — every authored door refuses it, flat and in the bag', () => { + describe.each(DOORS)('%s', (_door, parse) => { + it('CONTROL: `properties.resizable` parses', () => { + const r = parse({ ...AUTHORED, properties: { objectName: 'task', resizable: false } }); + expect(r.success, JSON.stringify(codeAndPath(r))).toBe(true); + }); + + it('`properties.resizableColumns` is refused at its path', () => { + expect(codeAndPath(parse({ ...AUTHORED, properties: { objectName: 'task', resizableColumns: false } }))) + .toEqual([{ code: 'invalid_type', path: ['properties', 'resizableColumns'] }]); + }); + + it('a flat `resizableColumns` on the node is refused by name', () => { + expect(codeAndPath(parse({ ...AUTHORED, resizableColumns: false }))) + .toEqual([{ code: 'invalid_type', path: ['resizableColumns'] }]); + }); + + it('an `object-view` `table.resizableColumns` stays refused by the slot\'s own message', () => { + // The slot overrides the grid's tombstone with its own refusal (objectui#10976), + // as it does for the five tombstones objectui#11068 retired on the grid. + const r = parse({ ...VIEW, table: { resizableColumns: false } }); + expect(codeAndPath(r)).toEqual([{ code: 'invalid_type', path: ['table', 'resizableColumns'] }]); + expect(r.success ? '' : r.error!.issues[0].message).toContain('objectui#10976'); + // Lit control: the slot relays `resizable`. + expect(parse({ ...VIEW, table: { resizable: false } }).success).toBe(true); + }); + }); + + it('the `object-view` mirror\'s own slot agrees with the doors above', () => { + expect(codeAndPath(ObjectViewSchema.safeParse({ ...VIEW, table: { resizableColumns: false } }))) + .toEqual([{ code: 'invalid_type', path: ['table', 'resizableColumns'] }]); + }); +}); + +describe('objectui#6152 round 7 — the TypeScript face', () => { + it('`resizableColumns` is a `tsc` error on the interface, while `resizable` type-checks', () => { + // A real directive: this package type-checks its tests (`tsconfig.test.json`), so a + // re-declared member fails on the unused directive. + // @ts-expect-error `resizableColumns` is RETIRED (objectui#6152) — write `resizable` + const retired: TsObjectGridSchema = { type: 'object-grid', objectName: 'task', resizableColumns: false }; + const live: TsObjectGridSchema = { type: 'object-grid', objectName: 'task', resizable: false }; + expect([retired.type, live.resizable]).toEqual(['object-grid', false]); + }); +}); + +/* + * Read off the member: a deletion would not compile here at all, and a re-declared + * `boolean` would read as `boolean | undefined`. + */ +type Equal = (() => T extends A ? 1 : 2) extends (() => T extends B ? 1 : 2) ? true : false; +type Expect = T; + +export type assertionResizableColumnsIsRetiredOnTheInterface = [ + Expect>, + Expect>, +]; diff --git a/packages/types/src/__tests__/zod-mirror-parity.test.ts b/packages/types/src/__tests__/zod-mirror-parity.test.ts index e6a3a84a7a..12a6d205ca 100644 --- a/packages/types/src/__tests__/zod-mirror-parity.test.ts +++ b/packages/types/src/__tests__/zod-mirror-parity.test.ts @@ -396,7 +396,12 @@ * and 37 / 53 until objectui#7344 swept the string / `z.any()` handler mirrors: * `DetailSchema` and `DetailViewSchema` entered (one `onBack` each) and * `CalendarViewSchema` grew by `onEventClick`. - * - **5 entries** in `UnmirroredDeclared`, **6 keys** across them — 5 / 16 until + * - **4 entries** in `UnmirroredDeclared`, **5 keys** across them — 5 / 6 until + * objectui#6152 round 7 RETIRED `ObjectGridSchema.resizableColumns` on both faces + * (`?: never` on the interface, a `retirementTombstone()` on the mirror, and + * `ObjectGrid`'s fallback read dropped) once `@objectstack/spec` 17.7.0 refused it in + * the `object-grid` row. A tombstone is a mirrored member, so the entry's one key went + * and the entry with it — ⛔ nothing mirrored, nothing reclassified; 5 / 16 until * objectui#6152 round 6 MIRRORED ten of `ObjectGridSchema`'s eleven keys, each * measured READ by `ObjectGrid` (a type-checker census and a runtime probe through the * real registry) and each a member of the spec's `object-grid` row — four as the spec @@ -591,8 +596,12 @@ * two. ⭐ The direction is the one this ledger is meant to drain in: a * runtime-only key leaves by being declared on the mirror, never by being * quietly refiled. - * **0 of the 5** are a subset of the **5** pairs above, so - * the union of the two unmirrored ledgers is **10** pairs, being **5** from `UnmirroredDeclared` plus **5** recorded ONLY in `RuntimeOnlyDeclared`. + * **0 of the 5** are a subset of the **4** pairs above, so + * the union of the two unmirrored ledgers is **9** pairs, being **4** from `UnmirroredDeclared` plus **5** recorded ONLY in `RuntimeOnlyDeclared`. + * ⭐ objectui#6152 round 7 moved the union from 10 to 9 and nothing else: the entry it + * closed (`ObjectGridSchema`, whose one key `resizableColumns` it retired on both + * faces) was in `UnmirroredDeclared` alone, so the shared count stays zero. It read + * `0 of the 5` against `5` pairs, a union of `10`, until then. * ⭐ objectui#11434's reader change moved the union from 11 to 10 and nothing else: the * designer entry it closed (`ProcessDesignerSchema`, whose `lanes` and `version` it * mirrored) was in `UnmirroredDeclared` alone, so the shared count stays zero. It read @@ -629,13 +638,16 @@ * drained it by MIRRORING the key as a named refusal. ⛔ The difference figure * is not recoverable by editing this sentence — it is a fact about the ledgers, * and it returns only when some pair is again runtime-only and nothing else. - * ⚠️ Every live figure in these two sentences is pinned: the `0` (a quantity - * of its own — how many entries the two unmirrored ledgers share), the `13` (the - * union) and the `5` after `plus` (the pairs recorded only as runtime-only), and - * three RESTATEMENTS of counts already stated above — the `5` in `of the 5` and - * both `8`s. (This list was amended by objectui#6152 round 3, which reshaped the - * sentence, and re-read by rounds 4 and 5, by objectui#10859 batch 7 and by - * objectui#11355 round 2, which moved only its digits.) + * ⚠️ Every live figure in these two sentences is pinned: the shared count before + * `of the` (a quantity of its own — how many entries the two unmirrored ledgers + * share), the union, and the figure after `plus` (the pairs recorded only as + * runtime-only), and three RESTATEMENTS of counts already stated above — the + * `RuntimeOnlyDeclared` entry count after `of the`, and both restatements of the + * `UnmirroredDeclared` entry count. (This list was amended by objectui#6152 round 3, + * which reshaped the sentence, and re-read by rounds 4 and 5, by objectui#10859 + * batch 7 and by objectui#11355 round 2, which moved only its digits. objectui#6152 + * round 7 names the figures by role, not by digit: the digits this list used to + * spell had gone stale while the pinned ones moved.) * They were spelled as English WORDS * until objectui#8222, which is why no instrument had ever read them: a figure * spelled "six" rots exactly as fast as one spelled `6`, it is just harder to @@ -2785,7 +2797,7 @@ interface KnownDrift { * * objectui#6058 seeded this ledger at **121 keys**, and — on ONE line, because the * pin below reads this sentence off disk — - * **6 keys** is what this ledger records today. + * **5 keys** is what this ledger records today. * The movements between the two are different facts. objectui#6152 measured the 23 * callback-shaped (`on*`) keys and ruled that mirroring is the wrong remedy for every * one of them; @@ -2826,6 +2838,10 @@ interface KnownDrift { * round 6 MIRRORED ten of `ObjectGridSchema`'s eleven keys, each measured READ — all of it * by REPAIR — and stopped at the eleventh, `resizableColumns`, whose ruled retirement * objectui#11276's move of the authored node into the spec's `properties` bag left open. + * Then objectui#6152 round 7 RETIRED that eleventh key on both faces, once + * `@objectstack/spec` 17.7.0 refused it in the `object-grid` row: `?: never` on the + * interface, a `retirementTombstone()` on the mirror and `ObjectGrid`'s fallback read + * dropped — a shrink by RETIREMENT, ⛔ not a repair, and the entry went with its one key. * * ## The decomposition of "121" — a reading at NAMED REVISIONS, so it cannot rot * @@ -2911,7 +2927,10 @@ interface KnownDrift { * explicit that forcing the 121 per-key decisions now would be wrong. Two splits * are recorded here so whoever works them off does not re-derive them: * - * - **SPEC-DERIVED (3 entries, 4 keys)** — it was 3 / 14 until objectui#6152 round 6 + * - **SPEC-DERIVED (2 entries, 3 keys)** — it was 3 / 4 until objectui#6152 round 7 + * RETIRED `ObjectGridSchema.resizableColumns` on both faces (a tombstone is a mirrored + * member): the entry emptied, so the half lost an entry and a key, and ⛔ nothing was + * mirrored. It was 3 / 14 until objectui#6152 round 6 * MIRRORED ten of `ObjectGridSchema`'s eleven keys: the entry kept `resizableColumns`, * so it stayed in this half and only the keys moved, all ten by REPAIR. It was 4 / 16 * until objectui#6152 round 3 @@ -3104,9 +3123,11 @@ interface KnownDrift { * `version` got their reader and were MIRRORED — one entry and two keys gone. objectui#6152 * round 6 then moved only the SPEC-DERIVED half, by the REPAIR route: ten of * `ObjectGridSchema`'s eleven keys MIRRORED, the entry kept `resizableColumns`, so no entry - * left and the LOCAL half did not move. The ledger + * left and the LOCAL half did not move. objectui#6152 round 7 then moved only the + * SPEC-DERIVED half again, by the RETIREMENT route: `resizableColumns` retired on both + * faces, so `ObjectGridSchema`'s entry left with its one key. The ledger * now totals — on ONE line, because the pin below reads this sentence off disk — - * **5 entries / 6 keys** — 3 / 4 spec-derived, 2 / 2 local. + * **4 entries / 5 keys** — 2 / 3 spec-derived, 2 / 2 local. * * ⛔ The four split figures above and this totals line are PINNED: 'objectui#7279' * at the bottom of this file derives every one of them from the `UnmirroredDeclared` @@ -3300,52 +3321,21 @@ interface UnmirroredDeclared { // single shrink by REPAIR), and 26 before that: the five `on*` keys went to // `RuntimeOnlyDeclared` at objectui#6152's reclassification and left it by // objectui#7804's named refusals. - /** - * SPEC-DERIVED by MEMBERSHIP since objectui#7762, LOCAL before it: that card mirrored - * `exportOptions` as the spec's OBJECT ARM by reference, which puts a `Spec…` symbol in - * this mirror's initializer, so `SPEC_DERIVED_PAIRS` re-derives the pair — the same - * mechanism that moved `ObjectViewSchema` at objectui#7779, and the second pair to reach - * this half by a single mirrored member. - * - * ⚠️ Read the half as MEMBERSHIP, not as a remedy: the key below is a plain - * hand-written omission on a `BaseSchema.extend({…})` mirror — the ordinary local - * route (objectui#6152's worklist), NOT objectui#2231's unification question, which is - * what the SPEC-DERIVED half means for a mirror that IS the spec schema by reference. - * This is the per-key reading objectui#7279 recorded on `ObjectViewSchema` for the same - * reason, kept here so whoever works these off does not re-derive it. - * - * ⚠️ `resizableColumns` is NOT mirroring debt in waiting, and ⛔ it is not mirrored: the - * seat's answer A on objectui#6152 retires it (`resizable` is canonical; the spec half - * forks to objectstack). objectui#6152 round 6 STOPPED at it rather than execute that - * answer, because objectui#11276 moved the AUTHORED `object-grid` node to the spec's - * `properties` bag after the answer was given: the bag is `ComponentPropsMap['object-grid']` - * by reference, which still declares `resizableColumns` ("Alternate spelling of - * `resizable`"), so retiring the key here and dropping `ObjectGrid`'s read would leave - * the authored bag accepting a key nothing reads. Its route is the open question on that - * round's report; the key stays here until it is answered. - * - * It was 11 until objectui#6152 round 6 MIRRORED ten keys, each measured READ by - * `ObjectGrid` (a type-checker census, and a runtime probe through the real registry - * that varied one key at a time) and each a member of the spec's `object-grid` row: - * `grouping`, `navigation`, `rowColor` and `rowHeight` as the spec schemas the twin - * names, BY REFERENCE; `reorderableColumns` and `singleClickEdit` as the row's own - * members, BY REFERENCE; `aggregations`, `bulkActionDefs`, `conditionalFormatting` and - * `operations` as restatements of the twin's local shapes (the conditional-formatting - * rule shared with `ListViewSchema`'s mirror). A shrink by REPAIR, the objectui#6639 - * route; the entry kept one key, so only the key total moved. - * - * It was 14 until objectui#11068 MIRRORED `emptyState` (read by `ObjectGrid` from that - * card on) and TOMBSTONED `rowSpecActions` / `bulkSpecActions` (second spellings of - * `rowActions` / `bulkActions` that nothing read) — a tombstone is a mirrored member. - * - * It was 15 until objectui#7762 MIRRORED `exportOptions` — the ledger's shrink by REPAIR - * on the route objectui#6639 opened, and the first one that also moved its entry between - * the split's halves. It was 17: `onNavigate` is in `RuntimeOnlyDeclared` below - * (objectui#6152), and `title` was MIRRORED by objectui#6639 (census-directed ruling - * 2026-08-29, declare branch) — the ledger's first shrink by REPAIR rather than - * reclassification. - */ - 'objectql.zod.ts#ObjectGridSchema': 'resizableColumns'; + // `objectql.zod.ts#ObjectGridSchema` recorded `resizableColumns` here (SPEC-DERIVED by + // membership since objectui#7762, as a plain hand-written omission) until objectui#6152 + // round 7 RETIRED it on both faces, on the seat's answer that `resizable` is canonical + // and the alias retires with no window: `?: never` on the interface, a + // `retirementTombstone()` on the mirror naming `properties.resizable`, and `ObjectGrid`'s + // `resizable ?? resizableColumns` fallback read dropped. It waited on the spec half: + // once objectui#11276 made the authored node's `properties` bag the spec row by + // reference, a local retirement alone would have left that bag accepting a key nothing + // read, so round 6 STOPPED at it, and round 7 ran only once `@objectstack/spec` 17.7.0 + // (objectstack#21445), which objectui resolves since objectui#11717, refused it in the + // row. ⛔ Nothing was mirrored: a tombstone is a mirrored member, so one key off a + // one-key entry took the entry with it. It was 11 keys until round 6 MIRRORED ten, each + // measured READ; 14 until objectui#11068; 15 until objectui#7762; and 17 until + // `onNavigate` went to `RuntimeOnlyDeclared` (objectui#6152) and objectui#6639 MIRRORED + // `title`. // `reports.zod.ts#ReportComponentSchema` recorded three keys here (LOCAL). objectui#6152 // round 3 MIRRORED `conditionalFormatting`, which `ReportViewer` reads per cell, and // round 4 RETIRED the other two on both faces under ADR-0049 enforce-or-remove: @@ -3381,7 +3371,7 @@ interface UnmirroredDeclared { * * `UnmirroredDeclared` above was seeded at **121 keys** by objectui#6058, and — on * ONE line, because the pin below reads this sentence off disk — - * `UnmirroredDeclared` records **6 keys** today. + * `UnmirroredDeclared` records **5 keys** today. * These 23 moved here whole. Keys have since left that ledger by MIRRORING and by * RETIREMENT, but the move recorded HERE is neither and repaired nothing. ⛔ Nothing * was mirrored by it, no declaration was removed, no defect was repaired and nothing was diff --git a/packages/types/src/objectql.ts b/packages/types/src/objectql.ts index 74ef991f26..b1d0a398a2 100644 --- a/packages/types/src/objectql.ts +++ b/packages/types/src/objectql.ts @@ -1125,10 +1125,24 @@ export interface ObjectGridSchema extends BaseSchema { defaultFilters?: Record; /** - * @deprecated Moved to top-level resizable - * Legacy resizable columns flag + * RETIRED (objectui#6152 round 7, ADR-0049) — the legacy second spelling of + * {@link ObjectGridSchema.resizable}, read only when `resizable` was absent. + * `@objectstack/spec` 17.7.0 (objectstack#21445) converted + * `ComponentPropsMap['object-grid'].resizableColumns` to a retired-key + * tombstone, so the protocol refuses an authored value by name, written flat + * on the node or in its `properties` bag; and `ObjectGrid` no longer reads it + * (its `resizable ?? resizableColumns` fallback went in the same change). + * + * `?: never`, not a deletion — this package's tombstone convention (see + * `defaultSort` above): a deletion is refused only on a fresh literal, so the + * retired spelling would type-check green through a widened value and do + * nothing. Lockstep with the Zod twin (`zod/objectql.zod.ts`, + * `retirementTombstone()`). + * + * @deprecated RETIRED (objectui#6152) — write `resizable` instead (on an + * authored node, `properties.resizable`). */ - resizableColumns?: boolean; + resizableColumns?: never; /** * @deprecated Use label instead diff --git a/packages/types/src/zod/objectql.zod.ts b/packages/types/src/zod/objectql.zod.ts index 27aee11ead..59edcee78a 100644 --- a/packages/types/src/zod/objectql.zod.ts +++ b/packages/types/src/zod/objectql.zod.ts @@ -303,6 +303,19 @@ const OBJECT_GRID_SHOW_FILTERS_RETIRED = + '`showFilters`, so an authored value drew nothing. The filter builder is the `list-view` toolbar\'s: ' + 'author a `list-view` and switch the builder with its `userActions.filter`. To narrow the rows this ' + 'grid fetches, write `filter`.'; +/** + * objectui#6152 round 7 — the same one-string shape for `resizableColumns`, retired + * with `ObjectGrid`'s fallback read of it. It names `properties.resizable` because an + * AUTHORED `object-grid` takes its props in the spec row's `properties` bag + * (objectui#11276); a node composed in code writes `resizable` on the node, and the + * hoist makes the two spellings one key. + */ +const OBJECT_GRID_RESIZABLE_COLUMNS_RETIRED = + 'RETIRED (objectui#6152, ADR-0049) — `resizableColumns` was the legacy second spelling of `resizable`, ' + + 'read only when `resizable` was absent, and `ObjectGrid` no longer reads it, so an authored value would ' + + 'draw nothing; `@objectstack/spec` 17.7.0 refuses it by name on `object-grid` too. Rename the key to ' + + '`resizable` — on an authored node, `properties.resizable`: ' + + '`{ "type": "object-grid", "properties": { "resizable": false } }`.'; /** * objectui#11070 — ONE `.describe()` string for every object-bound arm that declares @@ -627,6 +640,17 @@ export const ObjectGridSchema = BaseSchema.extend({ + 'Rename the key to `sort` and wrap the value in an array: `sort: [{ field, order }]`.', ), defaultFilters: z.record(z.string(), z.any()).optional(), + // objectui#6152 round 7 — RETIRED under ADR-0049, in lockstep with the `?: never` + // twin on the interface. `@objectstack/spec` 17.7.0 (objectstack#21445) refuses + // this key BY NAME on `object-grid` (a retired-key tombstone in the row that + // judges the authored `properties` bag), and `ObjectGrid` dropped its + // `resizable ?? resizableColumns` fallback in the same change, so a declared + // member here would parse green and draw nothing. A tombstone rather than a + // deletion, as for `defaultSort`: `BaseSchema` is `.passthrough()`, so an + // undeclared key is KEPT unexamined, not refused. The `object-view` `table` + // slot below keeps its own refusal of the key, which overrides this member + // there, as it does for the five objectui#11068 retired. + resizableColumns: retirementTombstone(OBJECT_GRID_RESIZABLE_COLUMNS_RETIRED), // The legacy caption/export-title fallback — `ObjectGrid.tsx` draws it at // exactly two sites, the export file name's `viewLabel` and the table // `caption`, each resolving `label` first and this key only when `label` @@ -734,8 +758,8 @@ export const ObjectGridSchema = BaseSchema.extend({ // row's own member (`reorderableColumns`, `singleClickEdit`), and // `conditionalFormatting` is the spec list view's own rule BY REFERENCE (the shared // `ConditionalFormattingRuleSchema` above, objectui#11533); the rest restate the - // twin's local shapes. ⛔ `resizableColumns`, the eleventh, is NOT here: its route - // is open on objectui#6152 (the entry in `zod-mirror-parity.test.ts` says why). + // twin's local shapes. ⛔ `resizableColumns`, the eleventh, was never mirrored: + // objectui#6152 round 7 RETIRED it (the tombstone above, beside `defaultFilters`). aggregations: z.array(ObjectGridAggregationSchema).optional().describe('Per-group aggregations drawn in each group header, e.g. [{ field: "amount", type: "sum" }]'), bulkActionDefs: z.array(ObjectGridBulkActionDefSchema).optional().describe('Rich bulk action definitions; each opens the bulk action dialog (params, confirm, progress) for the selected rows'), conditionalFormatting: z.array(ConditionalFormattingRuleSchema).optional().describe('Conditional formatting rules for row styling — `[{ condition, style }]`, the rules a list view declares: the first rule whose CEL `condition` holds applies its CSS `style` map to the row'), @@ -1062,7 +1086,8 @@ export const ObjectFormSchema = BaseSchema.extend({ * itself declares (`body`, `children`, `defaultSort`) are inherited unchanged; * the five it retired later (`name`, `placeholder`, `rowSpecActions`, * `bulkSpecActions`, `showFilters`, objectui#11068) are overridden below by this - * slot's own refusal. + * slot's own refusal, and so is `resizableColumns`, which the slot withheld as + * a legacy alias before objectui#6152 round 7 retired it on the grid itself. */ const tableKeyRefusal = (key: string, why: string) => retirementTombstone(