Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit d73d987

Browse files
feat(types)!: retire object-grid's resizableColumns on both faces and stop ObjectGrid reading it (part of objectui#6152, round 7) (#11955)
Part of #6152 (round 7) The card stays open for its later rounds, which triage booked as separate PRs: the 17.7.0 member moves (`operations`, `conditionalFormatting`, `filter`) and `ObjectCalendarSchema.calendar`'s `.passthrough()`. Neither is in this PR. Clause-②: no This round narrows and widens nothing. It retires `object-grid`'s `resizableColumns`, which `@objectstack/spec` 17.7.0 already refuses, so it owes one at-tier contract review before the queue. ## What changed: the four strokes the seat scoped The seat answered (card comment `5957903936`) that `resizable` is canonical and `resizableColumns` retires with no window, once the spec half was installable. `@objectstack/spec` 17.7.0 retired the key in `ComponentPropsMap['object-grid']` (objectstack#21445), and objectui resolves 17.7.0 since objectui#11717. 1. **TypeScript face.** `ObjectGridSchema.resizableColumns` is `?: never`, following the package's convention for a retired member (the `defaultSort` precedent on the same interface). The `@deprecated` tag points at `resizable`, which is `properties.resizable` on an authored node. 2. **Flat zod mirror.** The flat `ObjectGridSchema` in `@object-ui/types/zod` declares `resizableColumns: retirementTombstone(...)`. Its one-string message, the `OBJECT_GRID_RESIZABLE_COLUMNS_RETIRED` constant beside the objectui#11068 constants, names `properties.resizable`. It sits next to `defaultSort`'s tombstone. The `object-view` `table` slot keeps its own refusal, which `.extend`s over this member, just as it does for the five keys objectui#11068 retired on the grid. 3. **Renderer.** `ObjectGrid` hands `DataTable` `resizableColumns: schema.resizable ?? true`. The `?? schema.resizableColumns` fallback is gone. The `object-grid` registration's `resizable` input description no longer calls the old spelling a fallback. The registration's header comment now names `resizableColumns` beside `defaultSort` as a spelling that left by tombstone, not by exemption. 4. **Ledger.** The `objectql.zod.ts#ObjectGridSchema` entry leaves `UnmirroredDeclared`. Its one key was retired, and a tombstone is a mirrored member, so the entry went with it, as `PaginationSchema`'s did. The entry's docblock is rewritten as the departed-entry comment its neighbours use and keeps the entry's history. Every pinned prose figure moved and gained its history sentence: the header bullet, the split's SPEC-DERIVED line, the totals line and its narrative, both ledger docstrings, and the cross-ledger union sentence. The objectui#7279, #7733, #8222 and #8243 pins derive the new figures and are green: `UnmirroredDeclared` 4 entries / 5 keys, 2 / 3 spec-derived and 2 / 2 local, with a union of 9. No `KnownDrift`, `RuntimeOnlyDeclared`, `WiderThanDeclared` or `MirroredUndeclared` row moved. `SPEC_DERIVED_PAIRS` keeps `ObjectGridSchema`, because its membership comes from the mirror's spec references, which did not change. ## What moves for a reader of the published packages - **Did not move: the authored door.** `{ "type": "object-grid", "properties": { "resizableColumns": false } }` and the flat spelling on the node were already refused by name before this change, through the spec row and `flatPropRefusals`. That holds on the tolerant face, the strict face and `safeValidateSchema`, and the round-7 pin re-measures all three. - **Narrowed: `@object-ui/types`.** The TypeScript member now refuses any value. The exported flat `ObjectGridSchema` mirror refuses the key at the key with `invalid_type`, where `BaseSchema`'s `.passthrough()` used to keep it unexamined. - **Changed behaviour: `@object-ui/plugin-grid`.** A grid node that still carries `resizableColumns` gets the default, resizable columns. `resizableColumns: false` no longer removes the resize handles. A tree-scoped search found no producer that writes the key on an `object-grid` (see Measurements). - **Changeset.** `.changeset/6152-resizable-columns-retired.md` bumps `@object-ui/types` and `@object-ui/plugin-grid` `minor` (the fixed group's version policy) and states the break. ## Measurements before editing (the dispatch's mechanism assumptions) - **H1, the sites: held,** on base `19a7348`, which is the dispatch's `f1781be` plus one app-shell commit. The one renderer read is the `?? schema.resizableColumns` fallback in `ObjectGrid`. The registration names the spelling in the `resizable` input's description and in the header comment. The flat mirror did not carry the key, and the `table` slot refused it by name. - **H2, the install surface already refuses it: held** on the installed `@objectstack/spec` 17.7.0. `ComponentPropsMap['object-grid'].safeParse({ resizableColumns: true })` fails with `invalid_type` at `resizableColumns` and the retired-key message naming `resizable`. The lit control `{ resizable: true }` parses, and the strictness control (an unknown key) fails with `unrecognized_keys`. So the local tombstone refuses nothing the installed protocol accepts. The round-7 pin re-reads this against the installed package. - **H3, the pins that flip: partly held.** The round-6 test "`resizableColumns` is NOT mirrored: its route is open, and the parity ledger keeps it" flips in place into the retired assertion: the key is a member, and `true` and `false` are each refused at the key. Two other pins do not flip, because they pin the `table` slot's own refusal, which this round leaves standing: - the round-6 test "`table.navigation` and `table.resizableColumns` stay withheld, refused by name"; - `object-view-slot-key-lists.test.ts`'s `alias` row. Their docstrings now record that the grid itself tombstones the key since round 7. No assertion was deleted. - **Fixture sweep, by the rule's consumer radius (the whole repository).** Zero tracked JSON, Markdown or MDX documents write `resizableColumns` on an `object-grid`; a control search for `resizable` finds 11. The one consumer test that writes the key past the type is plugin-view's table-relay test, which writes it to prove it is withheld; it is green. `DataTableSchema.resizableColumns` is a different block's own key and is untouched, as the claim requires. ## Tests and gates Run from the worktree root with repo-relative paths. Every build, suite, type-check and ablation went through `os-verify-lock.sh`. Each exit code was captured before any pipe. - **Build.** `turbo run build --concurrency=2` over `check-doc-snippet-types --build-filter`: `Tasks: 35 successful, 35 total`. The dist was checked after the build: - `packages/types/dist/objectql.d.ts` carries `resizableColumns?: never`; - the plugin-grid bundle reads `resizableColumns: e.resizable ?? !0` and has 0 hits for the old fallback. - **Type-checks,** both scripts echoed in the log, exit 0, 0 `error TS`: - `pnpm --filter @object-ui/types type-check` (`tsc --noEmit && tsc -p tsconfig.examples.json && tsc -p tsconfig.test.json`); - `pnpm --filter @object-ui/plugin-grid type-check` (`tsc --noEmit && tsc -p tsconfig.test.json`). `--listFilesOnly` shows both test projects include the touched and new test files. - **Suites,** with `--maxWorkers=2`: - `vitest run packages/types/`: `Test Files 360 passed (360)`, `Tests 9624 passed (9624)`. - `vitest run packages/plugin-grid/ apps/console/src/__tests__/registry-inputs-spec-parity.test.ts`: `Test Files 195 passed (195)`, `Tests 2077 passed (2077)`. A verbose re-run of the two named files read 255 console-parity tests and the 4 new grid tests, each passing. - plugin-view's `ObjectView.tableSlotRelay-10976.test.tsx`: 7 passed. - **Gates at the final head `06db65a6`,** all exit 0: - `check-changeset-presence` ("9 source file(s) of 2 released package(s) changed, and this change declares 1 changeset(s)"), `changeset:check`, `check:changeset-claims` ("No pending changeset names a file this change touches"; a search of pending changesets for `resizableColumns` and for ledger figures also found nothing), `check:pending-changeset-literals`; - `check:new-line-citations` ("0 new citation(s)"), `check:control-bytes`, `check:spec-symbols`, `check:test-path-roots`, `check:unreferenced-sources`, `check:handler-key-reads`, `check:vi-mock-specifiers`, `check-type-check-coverage` ("43/43"); - `check-governed-queue-guard --test` over the 10 paths: NOT GOVERNED. - **Doc and skill gates,** run on the built closure at `4a1feb41`, all exit 0: - `check:skill-examples`; - `check:doc-types`; - `check:doc-snippets` ("784 of 784 block(s) judged, 0 failed"); - `check:doc-examples`; - `check:readme-exports`; - `check:component-surface-parity` (report-only); - `check:prompt-keys`; - `check:designer-field-key-parity`. `06db65a6` changes only the plugin-grid pin's cast, which no doc gate reads. - **Lint, a declared narrowing.** `eslint --no-inline-config --format json` judged all 9 changed TS files and ignored none: 1 error and 261 warnings. No message sits on an added line once the pin's `any` was replaced by a cast through `unknown` (`06db65a6`). The one error is the inline-disabled `react-hooks/static-components` line in `ObjectGrid`'s empty-state icon, which this diff does not touch. Invariance: `eslint.config.js` declares no `parserOptions.project` or `projectService` (no type-aware linting), so this diff cannot move a verdict in an untouched file. The repo-wide `pnpm lint` is CI's. ## Reverse verification Predictions were written to scratch before each run. Each mutation ran through objectstack's `scripts/ablation-replace.mjs` (wrap mode) on committed HEAD `06db65a6`, inside a harness with a `trap` restore on absolute paths. Every leg was restored and proven: blob equal to the HEAD blob, `git diff HEAD` 0 bytes, porcelain 0 lines. No leg needed a dist step: every subject resolves to source through relative imports. - **A1, put the renderer's fallback read back** (anchor x1 to x0, blob `2ec85adb` to `ed7482d6`). The plugin-grid pin read `1 failed | 3 passed`, exactly the predicted row: "`resizableColumns: false` alone is not read", `expected +0 to be 2`. - **A2, delete the mirror's tombstone member** (blob `59edcee7` to `ece13ddb`). - `tsc -p tsconfig.test.json` exited 2 with exactly one `TS2322`, `"objectql.zod.ts#ObjectGridSchema"` not assignable to `never`, at `assertionUnmirroredMatchesLedger`. - vitest read `4 failed | 159 passed`, as predicted: the three mirror rows of the round-7 pin and the flipped round-6 test. Every authored-door and slot row stayed green, and so did `object-view-slot-key-lists` and the parity runtime pins. - **A3, re-declare the TypeScript member as `boolean`** (blob `b1d0a398` to `1caaa5d4`). `tsc -p tsconfig.test.json` exited 2 with the three predicted errors: - `TS2578`, the round-7 `@ts-expect-error` now unused; - `TS2344` at `assertionResizableColumnsIsRetiredOnTheInterface`; - `TS2322` at `assertionDriftMatchesLedger` (the mirror's `never` against a declared `boolean`). The first A3 attempt timed out in the lock queue (exit 99) before `tsc` ran. Its restore was proven, and it was re-run under the same slot. ## NOT MEASURED - `check:sdui-registration-pins`. It weighs registrations in a built console, and this diff adds and removes no registration; only one input's description text changed. - The full `pnpm test` farm and the repo-wide `pnpm lint`. Both are CI's. - The plugin-view and plugin-list suites beyond the relay test. No producer passes `resizableColumns` to `ObjectGrid` (the tree-scoped search above), and `ObjectView` withholds it by construction. - The at-tier contract record. The seat owes it on the landing head. ## Acceptance notes - **Prose repair inside the claimed file.** `zod-mirror-parity.test.ts`'s paragraph listing which cross-ledger figures are pinned spelled their values as digits (`13`, both `8`s). Those digits had gone stale before this round, while the pinned ones moved. Because this round moves the same figures, the paragraph now names them by role, not by digit. - **The registry-parity comment is now historical.** `apps/console`'s `registry-inputs-spec-parity.test.ts` comment on the harvested `object-grid.resizableColumns` row ends "Retiring the renderer's own read of the old spelling is objectui#6152's." That read is retired here, so the sentence is now history. It is outside this round's surface and was left as is. Carrier: the next PR that touches that file. - **Slot-scoped docs stay accurate.** `content/docs/plugins/plugin-view.mdx`, `packages/plugin-view/README.md` and the `ObjectView` relay comment still call `resizableColumns` a legacy alias the `table` slot withholds (write `resizable`). That stays true, because the slot's own refusal is unchanged. ## Deviations, declared - Commit trailers are AGENTS.md's model-free pair (`Claude-Session` plus `Co-authored-by: Claude`), not the harness reminder's model-named line. - This footer is the dispatch contract's session-URL form. Session: `https://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw` --- _Generated by [Claude Code](https://claude.ai/code/session_01DBZ9bntPZ7VKyQNtJeNsgw)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1c51e97 commit d73d987

10 files changed

Lines changed: 365 additions & 75 deletions
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
'@object-ui/types': minor
3+
'@object-ui/plugin-grid': minor
4+
---
5+
6+
feat(types)!: retire `object-grid`'s `resizableColumns` on both faces, and stop `ObjectGrid` reading it (objectui#6152, round 7)
7+
8+
**Retired (breaking).** `resizableColumns` was the legacy second spelling of `resizable` on
9+
`object-grid`, read only when `resizable` was absent. `@objectstack/spec` 17.7.0 retired it in the
10+
`object-grid` row (objectstack#21445), so an authored document that writes it, flat on the node or
11+
inside its `properties` bag, was already refused by name before this change. This change retires
12+
the rest of it, with no alias window:
13+
14+
- `@object-ui/types`: `ObjectGridSchema.resizableColumns` is now `?: never`, so writing it on a
15+
typed literal is a `tsc` error. The flat zod `ObjectGridSchema` in `@object-ui/types/zod` (the
16+
node as `ObjectGrid` reads it after the `properties` hoist, or as code composes it) refuses the
17+
key by name at the key, where `.passthrough()` used to keep it unexamined. The refusal names
18+
`properties.resizable`.
19+
- `@object-ui/plugin-grid`: `ObjectGrid` reads `resizable` alone. A node that still carries
20+
`resizableColumns` (one composed in code, past the type) now gets the default, resizable
21+
columns, as if it had written nothing: `resizableColumns: false` no longer turns the column
22+
resize handles off. The `object-grid` registration's `resizable` input no longer describes the
23+
old spelling as a fallback.
24+
25+
Rename the key to `resizable`; on an authored node, that is `properties.resizable`.
26+
27+
An `object-view`'s `table.resizableColumns` is unchanged: the slot already refused it by name.

‎packages/plugin-grid/src/ObjectGrid.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5431,7 +5431,11 @@ export const ObjectGrid: React.FC<ObjectGridComponentProps> = ({
54315431
// `editable` produces no such column today. Following the same verdict is
54325432
// what makes the gated grid identical to the non-editable one.
54335433
rowActions: !!(inlineEditable && hasActions),
5434-
resizableColumns: schema.resizable ?? schema.resizableColumns ?? true,
5434+
// `resizable` alone (objectui#6152 round 7): the legacy `resizableColumns`
5435+
// spelling is retired on both faces and by `@objectstack/spec` 17.7.0, so
5436+
// it is no longer read here. (`resizableColumns` on the left is DataTable's
5437+
// own prop, a different block's key.)
5438+
resizableColumns: schema.resizable ?? true,
54355439
reorderableColumns: schema.reorderableColumns ?? false,
54365440
// [#5143] The authored key ∧ this principal's write verdict on the object.
54375441
editable: inlineEditable,
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* objectui#6152 round 7 — `ObjectGrid` reads `resizable` alone.
3+
*
4+
* `resizableColumns` was the legacy second spelling of `resizable`, read only when
5+
* `resizable` was absent (`schema.resizable ?? schema.resizableColumns ?? true`). It is
6+
* retired on both faces of `@object-ui/types` and by `@objectstack/spec` 17.7.0, which
7+
* refuses it in the `object-grid` row, so the fallback read went in the same change:
8+
* a node that still carries the old spelling (one composed in code, past the type)
9+
* gets the default, resizable columns, exactly as if it had written nothing.
10+
*
11+
* Observed in the DOM: each resizable header cell draws one `.cursor-col-resize` drag
12+
* handle (`data-table`'s own markup, the one `columnStatePersistence.test.tsx` drags).
13+
*/
14+
import { describe, it, expect, beforeEach } from 'vitest';
15+
import { render, screen, waitFor } from '@testing-library/react';
16+
import '@testing-library/jest-dom';
17+
import React from 'react';
18+
import type { ObjectGridSchema } from '@object-ui/types';
19+
20+
import { ObjectGrid } from '../ObjectGrid';
21+
import { __clearRecordCrudVerdictCache } from '../hooks/useRecordCrudVerdicts';
22+
import { installExplainDouble } from './explainDouble';
23+
import { registerAllFields } from '@object-ui/fields';
24+
import { ActionProvider } from '@object-ui/react';
25+
26+
registerAllFields();
27+
28+
beforeEach(() => {
29+
__clearRecordCrudVerdictCache();
30+
installExplainDouble();
31+
localStorage.clear();
32+
});
33+
34+
const rows = [
35+
{ id: '1', name: 'Alice', amount: 100 },
36+
{ id: '2', name: 'Bob', amount: 200 },
37+
];
38+
39+
/** Render a two-column grid and count the header resize handles it draws. */
40+
async function resizeHandles(extra: Record<string, unknown>): Promise<number> {
41+
// Cast through `unknown`: the retired spelling is a `tsc` error on `ObjectGridSchema`
42+
// now, and this writes it on purpose, as a node composed past the type would.
43+
const schema = {
44+
type: 'object-grid',
45+
objectName: 'test_object',
46+
columns: [
47+
{ field: 'name', label: 'Name' },
48+
{ field: 'amount', label: 'Amount', type: 'number' },
49+
],
50+
data: { provider: 'value', items: rows },
51+
...extra,
52+
} as unknown as ObjectGridSchema;
53+
const { container, unmount } = render(
54+
<ActionProvider>
55+
<ObjectGrid schema={schema} />
56+
</ActionProvider>,
57+
);
58+
await waitFor(() => expect(screen.getByText('Alice')).toBeInTheDocument());
59+
const count = container.querySelectorAll('th .cursor-col-resize').length;
60+
unmount();
61+
return count;
62+
}
63+
64+
describe('objectui#6152 round 7 — `ObjectGrid` no longer reads `resizableColumns`', () => {
65+
it('CONTROL: with neither key, both columns draw a resize handle (on by default)', async () => {
66+
expect(await resizeHandles({})).toBe(2);
67+
});
68+
69+
it('LIT CONTROL: `resizable: false` turns the handles off, so the count can move', async () => {
70+
expect(await resizeHandles({ resizable: false })).toBe(0);
71+
});
72+
73+
it('`resizableColumns: false` alone is not read: the grid keeps its default handles', async () => {
74+
expect(await resizeHandles({ resizableColumns: false })).toBe(2);
75+
});
76+
77+
it('`resizable: false` still wins when the retired spelling says otherwise', async () => {
78+
expect(await resizeHandles({ resizable: false, resizableColumns: true })).toBe(0);
79+
});
80+
});

‎packages/plugin-grid/src/index.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ export const ObjectGridRenderer: React.FC<{ schema: any; [key: string]: any }> =
211211
* turned it into a retired-key tombstone the protocol refuses by name, and
212212
* objectui#5861 removed every renderer read of it (ADR-0049 enforce-or-remove).
213213
* It stays off this list because the contract refuses it, not by exemption.
214+
* `resizableColumns` followed the same way: spec 17.7.0 tombstoned it
215+
* (objectstack#21445), and objectui#6152 round 7 removed `ObjectGrid`'s
216+
* `resizable ?? resizableColumns` read, so `resizable` is its only spelling.
214217
*
215218
* ## `description`, `emptyState` and `keyboardNavigation`: the 17.6.0 keys
216219
*
@@ -298,7 +301,7 @@ const GRID_QUERY_INPUTS: ComponentInput[] = [
298301
// ── presentation ──────────────────────────────────────────────────────────
299302
{ name: 'rowHeight', type: 'enum', enum: ['compact', 'short', 'medium', 'tall', 'extra_tall'], description: 'Row density. An unrecognised value falls back to `compact` rather than erroring.' },
300303
{ name: 'frozenColumns', type: 'number', description: 'How many leading columns stay pinned while the grid scrolls horizontally.' },
301-
{ 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.' },
304+
{ 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).' },
302305
{ name: 'reorderableColumns', type: 'boolean', description: 'Let users drag columns into a different order.' },
303306
{ 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.' },
304307
{ name: 'rowColor', type: 'object', description: 'Rules that colour whole rows from a field value.' },

‎packages/types/src/__tests__/object-view-slot-key-lists.test.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,12 @@ const TABLE_WITHHELD_BY_REASON = {
152152
* of these to it.
153153
*/
154154
nodeLevel: ['ariaLabel', 'disabled', 'disabledOn', 'hidden', 'hiddenOn', 'responsiveStyles', 'style', 'testId', 'visible', 'visibleOn', 'visibleWhen'],
155-
/** The legacy alias of a relayed key: `bulkActions`, `resizable`. */
155+
/**
156+
* The legacy alias of a relayed key: `bulkActions`, `resizable`. `resizableColumns`
157+
* is also a retirement tombstone on `ObjectGridSchema` itself since objectui#6152
158+
* round 7 (`ObjectGrid` no longer reads it); the slot still refuses it with its own
159+
* message, as it does the `unread` five.
160+
*/
156161
alias: ['batchActions', 'resizableColumns'],
157162
} as const;
158163

@@ -163,7 +168,8 @@ const TABLE_WITHHELD_KEYS: readonly string[] = Object.values(TABLE_WITHHELD_BY_R
163168
* faces) that the slot keeps: they type nothing, and they carry the named
164169
* refusal and its guidance onto this face too. The five the grid retired in
165170
* objectui#11068 are in the withheld set above instead, refused by the slot's
166-
* own message.
171+
* own message, and so is `resizableColumns`, which the grid retired in
172+
* objectui#6152 round 7.
167173
*/
168174
const TABLE_INHERITED_TOMBSTONES = ['body', 'children', 'defaultSort'] as const;
169175

‎packages/types/src/__tests__/unmirrored-declared-round6-6152.test.ts‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,10 @@
2323
* The authored `object-grid` node does not move: its bag is the spec row by
2424
* reference, and a prop written flat on the node stays refused by name.
2525
*
26-
* `resizableColumns`, the eleventh key, is NOT mirrored. Its route is open on the
27-
* card, and `zod-mirror-parity.test.ts` keeps it in `UnmirroredDeclared`.
26+
* `resizableColumns`, the eleventh key, was NOT mirrored: its route was open on the
27+
* card until round 7 RETIRED it. It is a member of the mirror since then, as a
28+
* retirement tombstone that refuses every value, and `zod-mirror-parity.test.ts`
29+
* no longer ledgers it (`unmirrored-declared-round7-6152.test.ts` pins the rest).
2830
*/
2931
import { describe, it, expect } from 'vitest';
3032

@@ -92,8 +94,15 @@ describe('objectui#6152 round 6 — the flat `ObjectGridSchema` mirror judges te
9294
expect(refused.every((i) => i.path[0] === key), JSON.stringify(refused)).toBe(true);
9395
});
9496

95-
it('`resizableColumns` is NOT mirrored: its route is open, and the parity ledger keeps it', () => {
96-
expect('resizableColumns' in ObjectGridSchema.shape).toBe(false);
97+
it('`resizableColumns` was not mirrored but RETIRED (round 7): a member that refuses every value', () => {
98+
// Flipped by objectui#6152 round 7. This read `false` while the key's route was
99+
// open; the retirement made it a tombstone member, which is what a passthrough
100+
// mirror needs to refuse the key instead of keeping it unexamined.
101+
expect('resizableColumns' in ObjectGridSchema.shape).toBe(true);
102+
for (const value of [true, false]) {
103+
expect(codeAndPath(ObjectGridSchema.safeParse({ ...GRID, resizableColumns: value })))
104+
.toEqual([{ code: 'invalid_type', path: ['resizableColumns'] }]);
105+
}
97106
});
98107

99108
it('the members the twin takes from the spec by name are the spec\'s, by reference', () => {
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*
8+
* objectui#6152 round 7 — `ObjectGridSchema.resizableColumns` RETIRED on both faces.
9+
*
10+
* `resizable` is canonical, and `resizableColumns` (its legacy second spelling, read only
11+
* when `resizable` was absent) retires with no window. Round 6 stopped at the key: the
12+
* authored `object-grid` node's `properties` bag is the spec's
13+
* `ComponentPropsMap['object-grid']` row by reference (objectui#11276), and that row still
14+
* declared the alias, so a local retirement alone would have left the bag accepting a key
15+
* nothing read. `@objectstack/spec` 17.7.0 (objectstack#21445) retired it in the row, and
16+
* objectui resolves 17.7.0 since objectui#11717. So this round makes the four strokes:
17+
*
18+
* - `resizableColumns?: never` on the interface;
19+
* - a `retirementTombstone()` on the flat zod mirror, naming `properties.resizable`;
20+
* - `ObjectGrid`'s `resizable ?? resizableColumns` fallback read dropped (pinned in
21+
* `@object-ui/plugin-grid`, `ObjectGrid.resizableColumnsRetired-6152.test.tsx`);
22+
* - the key off `zod-mirror-parity.test.ts`'s `UnmirroredDeclared` (its entry went with
23+
* it: a tombstone is a mirrored member).
24+
*
25+
* The protocol block below is the precondition, re-read against the INSTALLED spec: the
26+
* local tombstone must never refuse what the installed protocol accepts.
27+
*/
28+
import { describe, it, expect } from 'vitest';
29+
30+
import { ObjectGridPropsSchema as SpecObjectGridPropsSchema } from '@objectstack/spec/ui';
31+
import type { ObjectGridSchema as TsObjectGridSchema } from '../objectql';
32+
import { ObjectGridSchema, ObjectViewSchema } from '../zod/objectql.zod.js';
33+
import { AnyComponentSchema, StrictAnyComponentSchema, safeValidateSchema } from '../zod/index.zod.js';
34+
35+
type Issue = { code: string; path: PropertyKey[]; message: string };
36+
type Parsed = { success: boolean; error?: { issues: readonly Issue[] } };
37+
type Parse = (doc: unknown) => Parsed;
38+
39+
const codeAndPath = (r: Parsed) => (r.success ? [] : r.error!.issues.map((i) => ({ code: i.code, path: i.path })));
40+
41+
const GRID = { type: 'object-grid', objectName: 'task' };
42+
const AUTHORED = { type: 'object-grid', properties: { objectName: 'task' } };
43+
const VIEW = { type: 'object-view', objectName: 'task' };
44+
45+
const DOORS: ReadonlyArray<readonly [string, Parse]> = [
46+
['the tolerant face', (d) => AnyComponentSchema.safeParse(d)],
47+
['the strict face', (d) => StrictAnyComponentSchema.safeParse(d)],
48+
['safeValidateSchema', (d) => safeValidateSchema(d)],
49+
];
50+
51+
describe('objectui#6152 round 7 — the installed protocol refuses `resizableColumns` (the precondition)', () => {
52+
it('the `object-grid` row refuses `resizableColumns` by name, and takes `resizable`', () => {
53+
expect(codeAndPath(SpecObjectGridPropsSchema.safeParse({ resizableColumns: false })))
54+
.toEqual([{ code: 'invalid_type', path: ['resizableColumns'] }]);
55+
// Lit control: the canonical spelling parses on the same row.
56+
expect(SpecObjectGridPropsSchema.safeParse({ resizable: false }).success).toBe(true);
57+
});
58+
});
59+
60+
describe('objectui#6152 round 7 — the flat `ObjectGridSchema` mirror refuses the retired spelling', () => {
61+
it('CONTROL: the minimal node and the canonical `resizable` parse', () => {
62+
expect(ObjectGridSchema.safeParse(GRID).success).toBe(true);
63+
expect(ObjectGridSchema.safeParse({ ...GRID, resizable: false }).success).toBe(true);
64+
});
65+
66+
it('`resizableColumns` is a MEMBER, so `.passthrough()` no longer keeps it unexamined', () => {
67+
expect('resizableColumns' in ObjectGridSchema.shape).toBe(true);
68+
});
69+
70+
it.each([true, false])('`resizableColumns: %s` is refused at the key, naming `properties.resizable`', (value) => {
71+
const r = ObjectGridSchema.safeParse({ ...GRID, resizableColumns: value });
72+
expect(codeAndPath(r)).toEqual([{ code: 'invalid_type', path: ['resizableColumns'] }]);
73+
expect(r.success ? '' : r.error.issues[0].message).toContain('properties.resizable');
74+
});
75+
});
76+
77+
describe('objectui#6152 round 7 — every authored door refuses it, flat and in the bag', () => {
78+
describe.each(DOORS)('%s', (_door, parse) => {
79+
it('CONTROL: `properties.resizable` parses', () => {
80+
const r = parse({ ...AUTHORED, properties: { objectName: 'task', resizable: false } });
81+
expect(r.success, JSON.stringify(codeAndPath(r))).toBe(true);
82+
});
83+
84+
it('`properties.resizableColumns` is refused at its path', () => {
85+
expect(codeAndPath(parse({ ...AUTHORED, properties: { objectName: 'task', resizableColumns: false } })))
86+
.toEqual([{ code: 'invalid_type', path: ['properties', 'resizableColumns'] }]);
87+
});
88+
89+
it('a flat `resizableColumns` on the node is refused by name', () => {
90+
expect(codeAndPath(parse({ ...AUTHORED, resizableColumns: false })))
91+
.toEqual([{ code: 'invalid_type', path: ['resizableColumns'] }]);
92+
});
93+
94+
it('an `object-view` `table.resizableColumns` stays refused by the slot\'s own message', () => {
95+
// The slot overrides the grid's tombstone with its own refusal (objectui#10976),
96+
// as it does for the five tombstones objectui#11068 retired on the grid.
97+
const r = parse({ ...VIEW, table: { resizableColumns: false } });
98+
expect(codeAndPath(r)).toEqual([{ code: 'invalid_type', path: ['table', 'resizableColumns'] }]);
99+
expect(r.success ? '' : r.error!.issues[0].message).toContain('objectui#10976');
100+
// Lit control: the slot relays `resizable`.
101+
expect(parse({ ...VIEW, table: { resizable: false } }).success).toBe(true);
102+
});
103+
});
104+
105+
it('the `object-view` mirror\'s own slot agrees with the doors above', () => {
106+
expect(codeAndPath(ObjectViewSchema.safeParse({ ...VIEW, table: { resizableColumns: false } })))
107+
.toEqual([{ code: 'invalid_type', path: ['table', 'resizableColumns'] }]);
108+
});
109+
});
110+
111+
describe('objectui#6152 round 7 — the TypeScript face', () => {
112+
it('`resizableColumns` is a `tsc` error on the interface, while `resizable` type-checks', () => {
113+
// A real directive: this package type-checks its tests (`tsconfig.test.json`), so a
114+
// re-declared member fails on the unused directive.
115+
// @ts-expect-error `resizableColumns` is RETIRED (objectui#6152) — write `resizable`
116+
const retired: TsObjectGridSchema = { type: 'object-grid', objectName: 'task', resizableColumns: false };
117+
const live: TsObjectGridSchema = { type: 'object-grid', objectName: 'task', resizable: false };
118+
expect([retired.type, live.resizable]).toEqual(['object-grid', false]);
119+
});
120+
});
121+
122+
/*
123+
* Read off the member: a deletion would not compile here at all, and a re-declared
124+
* `boolean` would read as `boolean | undefined`.
125+
*/
126+
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
127+
type Expect<T extends true> = T;
128+
129+
export type assertionResizableColumnsIsRetiredOnTheInterface = [
130+
Expect<Equal<TsObjectGridSchema['resizableColumns'], undefined>>,
131+
Expect<Equal<TsObjectGridSchema['resizable'], boolean | undefined>>,
132+
];

0 commit comments

Comments
 (0)