Skip to content

Commit e6ca40e

Browse files
os-litantclaude
andauthored
feat(spec): retire object-grid's legacy defaultSort fallback (#11805, ADR-0049) (#12256)
* feat(spec): retire object-grid's legacy defaultSort fallback (#11805, ADR-0049) Tombstone ObjectGridPropsSchema.defaultSort with retiredKey() — the legacy single-sort second spelling of sort, read only when sort was absent (measured at the .objectui-sha pin 190fbd01d, plugin-grid/src/ObjectGrid.tsx:1244-1246 and :2847). Register ui/ObjectGridProps:defaultSort in RETIRED_KEYS_BY_MAJOR[18], add the D2 conversion object-grid-default-sort-removed (wrap-and-rename to sort: [pair] when sort is absent; pure strip when sort is present) wired into the step-18 chain, regenerate the authorable surface ([RETIRED] mark) and the ui/component reference, and pin the refusal + no-materialize + surviving-sort behaviour in component.test.ts. Changeset ships minor per the maintainer's #11805 ruling (launch-window convention). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NDGG54XF5gbTLdQzCtnaVV * merge origin/main (os-regen artifacts taken from main; regeneration follows) * chore(spec): regenerate artifacts after merging origin/main (post-#12191) gen:migration-registry (byte-identical — main's generated regions already carried the #12191 semantic entries), rebuild restoring the ui/ObjectGridProps:defaultSort [RETIRED] surface mark, gen:docs restoring the retired reference row. Sibling entries and implementation bodies asserted present (field-max/min-length semantic entries, object editMode, minLength). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NDGG54XF5gbTLdQzCtnaVV --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0acadda commit e6ca40e

8 files changed

Lines changed: 367 additions & 6 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): retire `object-grid`'s legacy `defaultSort` fallback (#11805, ADR-0049)
6+
7+
<!-- adr-0087: registered object-grid-default-sort-removed -->
8+
9+
**BREAKING** accept-set narrowing, landing after the v17.0.0 cut (the lockstep
10+
launch-window convention ships it as `minor`, per the maintainer's #11805
11+
ruling — 「不需要major」; the migration prescription is registered under
12+
protocol major 18, where `os migrate meta` users will look).
13+
14+
`ObjectGridPropsSchema.defaultSort` was the legacy second spelling of `sort`:
15+
a single `{ field, order }` pair the renderer read only when `sort` was absent
16+
— measured at the `.objectui-sha` pin (`190fbd01d`),
17+
`plugin-grid/src/ObjectGrid.tsx:1244-1246` (the `$orderby` fetch fallback) and
18+
`:2847`, where the header-arrow path wraps it `[schema.defaultSort]`, the
19+
exact array shape `sort` carries. One intent, two spellings; objectui's mirror
20+
schema is parity-test-only and parses nothing at runtime, so only this
21+
strictObject can refuse the legacy spelling (objectui#5861 retires the
22+
renderer's reads as the consumer half, on its own schedule).
23+
24+
FROM → TO:
25+
26+
- `defaultSort: { field, order }` (no `sort` beside it) →
27+
`sort: [{ field, order }]` — the same pair, wrapped in the array shape every
28+
read path honours.
29+
- `defaultSort` beside an authored `sort`*(removed)*. The renderer's own
30+
precedence made the fallback unread there, so the deletion is lossless.
31+
32+
One-line fix: rename the key to `sort` and wrap the value in an array;
33+
`os migrate meta --from 17` lists the mechanical edits for existing sources.
34+
35+
The retirement kit:
36+
37+
- `retiredKey()` tombstone in `ObjectGridPropsSchema` — authoring the key is a
38+
tsc error (`never`) and a parse error carrying the wrap-and-rename
39+
prescription (the surface baseline line carries `[RETIRED]`)
40+
- ADR-0087 registration: `ui/ObjectGridProps:defaultSort` in
41+
`RETIRED_KEYS_BY_MAJOR[18]`, and the D2 conversion
42+
`object-grid-default-sort-removed` (protocol 18) wired into the step-18
43+
chain — wrap-and-rename when `sort` is absent, a pure strip when `sort` is
44+
present
45+
- pin tests (`component.test.ts`): a refusal pin asserting the prescription, a
46+
no-materialize pin, and a surviving-surface pin on `sort`
47+
- zero authored occurrences in either repo's corpora (the card's measurement,
48+
re-run at dispatch), so no in-repo source changes ride along

content/docs/references/ui/component.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ const result = AIChatWindowProps.parse(data);
275275
| **filter** | `any` | optional | Base query filter (ObjectQL filter array/AST) — lowered to the wire `$filter`. THE key #7750 misspelled as plural |
276276
| **defaultFilters** | `any` | optional | Legacy base-filter fallback, read only when `filter` is absent. Prefer `filter` |
277277
| **sort** | `any` | optional | Initial sort (array of `{ field, order }`) |
278-
| **defaultSort** | `any` | optional | Legacy single-sort fallback (`{ field, order }`), read only when `sort` is absent. Prefer `sort` |
278+
| **defaultSort** | `never` | optional | [REMOVED] `object-grid` property `defaultSort` was removed in @objectstack/spec 17 (#11805, ADR-0049) — it was the legacy second spelling of `sort`: a single `{ field, order }` pair read only when `sort` was absent, so one intent had two spellings and a grid authoring both silently ignored this one. Rename the key to `sort` and wrap the value in an array (`defaultSort: { field, order }` becomes `sort: [{ field, order }]`); the pair itself is unchanged. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. |
279279
| **pagination** | `any` | optional | Pagination config (`{ pageSize, pageSizeOptions, … }`); its presence enables paging |
280280
| **pageSize** | `number` | optional | Flat page-size shorthand; `pagination.pageSize` wins when both are set |
281281
| **showPagination** | `boolean` | optional | Show the pager (read only when `pagination` is absent) |

packages/spec/authorable-surface/ui.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -719,7 +719,7 @@
719719
"ui/ObjectGridProps:conditionalFormatting",
720720
"ui/ObjectGridProps:data",
721721
"ui/ObjectGridProps:defaultFilters",
722-
"ui/ObjectGridProps:defaultSort",
722+
"ui/ObjectGridProps:defaultSort [RETIRED]",
723723
"ui/ObjectGridProps:editable",
724724
"ui/ObjectGridProps:exportOptions",
725725
"ui/ObjectGridProps:fields",

packages/spec/src/conversions/registry.ts

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7901,6 +7901,198 @@ const pageComponentResponsiveRemoved: MetadataConversion = {
79017901
},
79027902
};
79037903

7904+
/**
7905+
* `object-grid`'s legacy single-sort fallback leaves the contract (protocol 18,
7906+
* #11805, ADR-0049 enforce-or-remove; maintainer ruling 2026-08-25,
7907+
* decision-inbox batch 4 — the producer half of objectui#5861, under the
7908+
* objectui#4869 「接受所有」 direction).
7909+
*
7910+
* `defaultSort` was the second spelling of `sort`: a SINGLE `{ field, order }`
7911+
* pair the renderer read only when `sort` was absent — measured at the
7912+
* `.objectui-sha` pin (`190fbd01d`), `plugin-grid/src/ObjectGrid.tsx:1244-1246`
7913+
* (the `$orderby` fetch fallback) and `:2847`, where the header-arrow path
7914+
* wraps it `[schema.defaultSort]`, the exact array shape `sort` carries. One
7915+
* intent, two spellings, and only the spec's strictObject can refuse the
7916+
* legacy one: objectui's mirror schema exists for parity tests and parses
7917+
* nothing at runtime, so stopping the reads without this producer-side
7918+
* retirement would turn a declared contract into a silent no-op.
7919+
*
7920+
* NOT a pure lossless delete, unlike {@link pageStructureInertKeysRemoved}:
7921+
* when `sort` is absent the fallback WAS the grid's sort, so the conversion
7922+
* carries the pair over — renamed to `sort` and wrapped in the array shape the
7923+
* renderer itself wraps it into at `:2847`. When `sort` is present the
7924+
* fallback was never read (the renderer's own precedence), and the key strips
7925+
* as a lossless delete. Zero authored occurrences in either repo's corpora
7926+
* (the card's measurement, re-run at dispatch), so this entry exists for
7927+
* stored `sys_metadata` rows and for authors outside the repo.
7928+
*
7929+
* objectui#5861 (retire the renderer's two reads) is the consumer half and
7930+
* proceeds on its own schedule after a released spec version reaches the pin.
7931+
*/
7932+
const objectGridDefaultSortRemoved: MetadataConversion = {
7933+
id: 'object-grid-default-sort-removed',
7934+
toMajor: 18,
7935+
retiredFromLoadPath: true,
7936+
surface: 'page.component.object-grid.defaultSort',
7937+
summary:
7938+
"object-grid component prop 'defaultSort' removed (#11805 — the legacy single-sort second "
7939+
+ "spelling of 'sort', read only when 'sort' was absent; the pair moves to sort: [{ field, "
7940+
+ 'order }], the array shape every read path honours)',
7941+
apply(stack, emit) {
7942+
return mapPageComponents(stack, (component, path) => {
7943+
if (component.type !== 'object-grid') return component;
7944+
const properties = component.properties;
7945+
if (!isDict(properties) || !('defaultSort' in properties)) return component;
7946+
if ('sort' in properties) {
7947+
// `sort` present: the fallback was never read — a pure lossless delete.
7948+
const stripped = stripKeys(properties, ['defaultSort'], emit, `${path}.properties`);
7949+
return { ...component, properties: stripped };
7950+
}
7951+
// `sort` absent: the fallback WAS the sort. Carry the pair, wrapped in
7952+
// the array shape `sort` reads (the renderer's own `[schema.defaultSort]`
7953+
// equivalence); an already-array value moves as-is — an array never
7954+
// worked in `defaultSort` (the fetch path read `.field` off it), and
7955+
// `sort` is where an array is legal.
7956+
const { defaultSort, ...rest } = properties;
7957+
emit({ from: 'defaultSort', to: 'sort', path: `${path}.properties.sort` });
7958+
return {
7959+
...component,
7960+
properties: { ...rest, sort: Array.isArray(defaultSort) ? defaultSort : [defaultSort] },
7961+
};
7962+
});
7963+
},
7964+
fixture: {
7965+
before: {
7966+
pages: [
7967+
{
7968+
name: 'work_queue',
7969+
regions: [
7970+
{
7971+
name: 'main',
7972+
components: [
7973+
// The fallback IS the sort: no `sort` beside it, so the pair
7974+
// moves — renamed and wrapped.
7975+
{
7976+
type: 'object-grid',
7977+
id: 'g1',
7978+
properties: { objectName: 'crm_task', defaultSort: { field: 'due_date', order: 'asc' } },
7979+
},
7980+
// Both spellings authored: `sort` wins (the renderer's own
7981+
// precedence), so the fallback strips as a lossless delete.
7982+
{
7983+
type: 'object-grid',
7984+
id: 'g2',
7985+
properties: {
7986+
objectName: 'crm_task',
7987+
sort: [{ field: 'priority', order: 'desc' }],
7988+
defaultSort: { field: 'due_date', order: 'asc' },
7989+
},
7990+
},
7991+
// `defaultSort` on a component that is not an object-grid —
7992+
// not this entry's key (the strip is scoped by component type,
7993+
// never by key name).
7994+
{
7995+
type: 'object-kanban',
7996+
id: 'k1',
7997+
properties: { objectName: 'crm_task', defaultSort: { field: 'due_date', order: 'asc' } },
7998+
},
7999+
// The nested position (#6775's lesson): a grid inside a card's
8000+
// `children` is still a component.
8001+
{
8002+
type: 'page:card',
8003+
id: 'c1',
8004+
properties: {
8005+
children: [
8006+
{
8007+
type: 'object-grid',
8008+
id: 'g3',
8009+
properties: { objectName: 'crm_lead', defaultSort: { field: 'created_at', order: 'desc' } },
8010+
},
8011+
],
8012+
},
8013+
},
8014+
],
8015+
},
8016+
],
8017+
},
8018+
// The named-slot shape (#6776): a grid authored into a slotted page.
8019+
{
8020+
name: 'work_queue_detail',
8021+
kind: 'slotted',
8022+
regions: [],
8023+
slots: {
8024+
details: {
8025+
type: 'object-grid',
8026+
id: 'g4',
8027+
properties: { objectName: 'crm_task', defaultSort: { field: 'updated_at', order: 'desc' } },
8028+
},
8029+
},
8030+
},
8031+
],
8032+
},
8033+
after: {
8034+
pages: [
8035+
{
8036+
name: 'work_queue',
8037+
regions: [
8038+
{
8039+
name: 'main',
8040+
components: [
8041+
{
8042+
type: 'object-grid',
8043+
id: 'g1',
8044+
properties: { objectName: 'crm_task', sort: [{ field: 'due_date', order: 'asc' }] },
8045+
},
8046+
{
8047+
type: 'object-grid',
8048+
id: 'g2',
8049+
properties: {
8050+
objectName: 'crm_task',
8051+
sort: [{ field: 'priority', order: 'desc' }],
8052+
},
8053+
},
8054+
{
8055+
type: 'object-kanban',
8056+
id: 'k1',
8057+
properties: { objectName: 'crm_task', defaultSort: { field: 'due_date', order: 'asc' } },
8058+
},
8059+
{
8060+
type: 'page:card',
8061+
id: 'c1',
8062+
properties: {
8063+
children: [
8064+
{
8065+
type: 'object-grid',
8066+
id: 'g3',
8067+
properties: { objectName: 'crm_lead', sort: [{ field: 'created_at', order: 'desc' }] },
8068+
},
8069+
],
8070+
},
8071+
},
8072+
],
8073+
},
8074+
],
8075+
},
8076+
{
8077+
name: 'work_queue_detail',
8078+
kind: 'slotted',
8079+
regions: [],
8080+
slots: {
8081+
details: {
8082+
type: 'object-grid',
8083+
id: 'g4',
8084+
properties: { objectName: 'crm_task', sort: [{ field: 'updated_at', order: 'desc' }] },
8085+
},
8086+
},
8087+
},
8088+
],
8089+
},
8090+
// Four notices: three wrap-and-renames (g1, the nested g3, the slotted g4)
8091+
// and one strip (g2, where `sort` already won).
8092+
expectedNotices: 4,
8093+
},
8094+
};
8095+
79048096
export const CONVERSIONS_BY_MAJOR: Readonly<Record<number, readonly MetadataConversion[]>> = {
79058097
11: [flowNodeHttpRename, pageKindJsxToHtml, flowNodeFilterAlias, objectCompactLayoutRename],
79068098
13: [stackRolesToPositions, owdLegacyReadAliases, sharingRecipientRoleToPosition],
@@ -7985,6 +8177,7 @@ export const CONVERSIONS_BY_MAJOR: Readonly<Record<number, readonly MetadataConv
79858177
mappingLookupParamsRemoved,
79868178
translationComponentSubmitLabelRemoved,
79878179
pageComponentResponsiveRemoved,
8180+
objectGridDefaultSortRemoved,
79888181
],
79898182
};
79908183

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
// #11805 — ADR-0049 enforce-or-remove (maintainer ruling 2026-08-25,
4+
// decision-inbox batch 4: 「#11805 退役 defaultSort,不需要major」; the producer
5+
// half of objectui#5861, under the objectui#4869 「接受所有」 direction).
6+
// `defaultSort` was the legacy second spelling of `object-grid`'s `sort`: a
7+
// single `{ field, order }` pair the renderer read only when `sort` was absent
8+
// (measured at the `.objectui-sha` pin `190fbd01d`,
9+
// `plugin-grid/src/ObjectGrid.tsx:1244-1246` fetch fallback and `:2847`, which
10+
// wraps it `[schema.defaultSort]` — the exact array shape `sort` carries). One
11+
// intent, two spellings; objectui's mirror schema is parity-test-only and
12+
// parses nothing at runtime, so only this repo's strictObject can refuse the
13+
// key. Zero authored occurrences in either repo's corpora (the card's
14+
// measurement, re-run here at dispatch).
15+
//
16+
// Registered under 18, not 17: v17.0.0 was cut before this landed, so the
17+
// removal ships on the 17.x line (launch-window convention: accept-set
18+
// narrowings ride minor releases) and the prescription lives at the major
19+
// boundary where `migrate meta` users look (the #8495 / PR #8666 precedent,
20+
// as `data/Metric:filters` before it). Tombstoned with `retiredKey()` in
21+
// `ObjectGridPropsSchema` (the surface baseline line carries `[RETIRED]`);
22+
// sources are rewritten by the D2 conversion `object-grid-default-sort-removed`
23+
// (wrap-and-rename to `sort: [pair]` when `sort` is absent; a pure lossless
24+
// delete when `sort` is present, since the fallback was never read then).
25+
export const entry = 'ui/ObjectGridProps:defaultSort';

packages/spec/src/migrations/registry.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5195,7 +5195,19 @@ const step18: MigrationStep = {
51955195
'never consulted (zero value reads; the only non-test occurrences were the ' +
51965196
'schema declaration and two type positions). The `kind` bucket itself and its ' +
51975197
'`id` are untouched; file-type discovery stays single-channel on `filePatterns`. ' +
5198-
'D3 semantic `plugin-manifest-kind-globs-retired`, same no-seam reasoning.',
5198+
'D3 semantic `plugin-manifest-kind-globs-retired`, same no-seam reasoning. ' +
5199+
'Finally, it retires `object-grid`\'s `defaultSort` (#11805, ADR-0049 enforce-or-remove; ' +
5200+
'maintainer ruling 2026-08-25, decision-inbox batch 4 — the producer half of ' +
5201+
'objectui#5861, under the objectui#4869 「接受所有」 direction): the legacy second ' +
5202+
'spelling of `sort`, a single `{ field, order }` pair the renderer read only when ' +
5203+
'`sort` was absent (measured at the `.objectui-sha` pin `190fbd01d`, ' +
5204+
'`plugin-grid/src/ObjectGrid.tsx:1244-1246` and `:2847`, which wraps it ' +
5205+
'`[schema.defaultSort]` — the exact array shape `sort` carries). One intent, two ' +
5206+
'spellings; objectui\'s mirror schema is parity-test-only and parses nothing at ' +
5207+
'runtime, so only the spec strictObject can refuse the key. The mechanical ' +
5208+
'conversion carries the pair over — renamed to `sort` and wrapped in the array ' +
5209+
'shape — when `sort` is absent, and strips it as a pure lossless delete when ' +
5210+
'`sort` is present (the renderer\'s own precedence made it unread then).',
51995211
conversionIds: [
52005212
'field-malformed-scale-precision-removed',
52015213
'record-chatter-position-vocabulary',
@@ -5208,6 +5220,7 @@ const step18: MigrationStep = {
52085220
'mapping-lookup-params-removed',
52095221
'translation-component-submit-label-removed',
52105222
'page-component-responsive-removed',
5223+
'object-grid-default-sort-removed',
52115224
],
52125225
semantic: [
52135226
// One file per entry under `entries/semantic/`, concatenated here sorted by
@@ -7447,6 +7460,29 @@ export const RETIRED_KEYS_BY_MAJOR: Readonly<Record<number, readonly string[]>>
74477460
// `element-input-target-variable-removed` (a page component IS a stack
74487461
// collection member, unlike the `kernel/Manifest:loading` family).
74497462
'ui/ElementTextInputProps:targetVariable',
7463+
// #11805 — ADR-0049 enforce-or-remove (maintainer ruling 2026-08-25,
7464+
// decision-inbox batch 4: 「#11805 退役 defaultSort,不需要major」; the producer
7465+
// half of objectui#5861, under the objectui#4869 「接受所有」 direction).
7466+
// `defaultSort` was the legacy second spelling of `object-grid`'s `sort`: a
7467+
// single `{ field, order }` pair the renderer read only when `sort` was absent
7468+
// (measured at the `.objectui-sha` pin `190fbd01d`,
7469+
// `plugin-grid/src/ObjectGrid.tsx:1244-1246` fetch fallback and `:2847`, which
7470+
// wraps it `[schema.defaultSort]` — the exact array shape `sort` carries). One
7471+
// intent, two spellings; objectui's mirror schema is parity-test-only and
7472+
// parses nothing at runtime, so only this repo's strictObject can refuse the
7473+
// key. Zero authored occurrences in either repo's corpora (the card's
7474+
// measurement, re-run here at dispatch).
7475+
//
7476+
// Registered under 18, not 17: v17.0.0 was cut before this landed, so the
7477+
// removal ships on the 17.x line (launch-window convention: accept-set
7478+
// narrowings ride minor releases) and the prescription lives at the major
7479+
// boundary where `migrate meta` users look (the #8495 / PR #8666 precedent,
7480+
// as `data/Metric:filters` before it). Tombstoned with `retiredKey()` in
7481+
// `ObjectGridPropsSchema` (the surface baseline line carries `[RETIRED]`);
7482+
// sources are rewritten by the D2 conversion `object-grid-default-sort-removed`
7483+
// (wrap-and-rename to `sort: [pair]` when `sort` is absent; a pure lossless
7484+
// delete when `sort` is present, since the fallback was never read then).
7485+
'ui/ObjectGridProps:defaultSort',
74507486
// #11027 — ADR-0049 enforce-or-remove (maintainer ruling 2026-08-22, ruled B:
74517487
// retire + repair the redirect texts in the same change). The LAST carrier of
74527488
// the `ResponsiveConfig` layout block, and the destination the

0 commit comments

Comments
 (0)