@@ -8702,6 +8702,76 @@ const step18: MigrationStep = {
87028702 + 'set stays exactly `DEFAULT_METADATA_TYPE_REGISTRY` plus item-population growth, '
87038703 + 'before and after.',
87048704 },
8705+ {
8706+ id: 'object-block-sort-item-array',
8707+ surface:
8708+ 'The `sort` prop of `object-grid` and `object-calendar` in `ComponentPropsMap` '
8709+ + '(the FORM: the accept-anything `z.unknown()` at both block doors, vs the '
8710+ + '`SortItem` array `[{ field, order }, ...]`)',
8711+ replacement:
8712+ '`z.array(SortItemSchema)` at both doors — the array `ElementDataSourceSchema.sort`, '
8713+ + '`ListPageSchema.sort` and `element:record_picker`\'s flat `sort` shorthand already '
8714+ + 'carry. The legacy OData-ish clause `sort: \'created_at desc\'` becomes '
8715+ + '`sort: [{ field: \'created_at\', order: \'desc\' }]`; a bare field name '
8716+ + '`sort: \'created_at\'` meant ascending and becomes '
8717+ + '`sort: [{ field: \'created_at\', order: \'asc\' }]` — `order` is required in '
8718+ + '`SortItemSchema`, so it is written out rather than omitted. A comma-separated '
8719+ + 'clause becomes one array entry per key, in the same order. `record:related_list` '
8720+ + 'is NOT moved by this entry: its string is the `\'field\'` / `\'-field\'` dialect '
8721+ + 'read by `RelatedList.normalizeSortSpec`, which never reaches '
8722+ + '`convertSortToQueryParams`, and retiring it was not ruled. '
8723+ + '`object-grid.defaultSort` is a different key, retired separately by the '
8724+ + '`ui__ObjectGridProps__defaultSort` entry.',
8725+ reason:
8726+ 'One `sort` spelling platform-wide, the array (objectui#8221, decision batch #77, '
8727+ + '2026-09-07, maintainer verbatim 「其他同意」, option B; the consumer half is '
8728+ + 'objectui PR #8758, which drops the string arm from `convertSortToQueryParams`). '
8729+ + 'Item 4 of that ruling is this entry\'s subject: 「`ComponentPropsMap` for '
8730+ + '`object-calendar` and `object-grid` constrains the `sort` value to the array shape '
8731+ + '(today it accepts anything), so the spec, the registrations and the helper agree; '
8732+ + 'that is a pull-back to the declared contract, ordinary tier」. The `z.unknown()` at '
8733+ + 'both doors was a read-point record (#7751), the same vintage as the `filter` doors '
8734+ + 'the `element-data-source-and-object-block-filter-rule-array` entry moved, and not an '
8735+ + 'exception to the ruling: measured on `@objectstack/spec` 17.2.0 an array, a string '
8736+ + 'and a bare NUMBER all returned `success: true` while `bogusProp` was refused by name '
8737+ + 'on the same call, so key checking was live and only the VALUE was unheld. Meanwhile '
8738+ + 'objectui\'s own html tier has published `type: \'array\'` for the grid all along '
8739+ + '(`plugin-grid/src/index.tsx:222`) and answered `type-mismatch` on the string — a '
8740+ + 'spelling `@object-ui/core` implemented, the docs taught and the validator refused, '
8741+ + 'which is what made this a ruling rather than a mechanical widening. '
8742+ + 'Sequenced measurement-first: at the objectui pin this repo builds against '
8743+ + '(`53ded82b`) the string is still lowered — `ObjectGrid.tsx:1844-1851` carries an '
8744+ + 'explicit `typeof === \'string\'` arm onto `$orderby`, and `ObjectCalendar.tsx:431` '
8745+ + 'hands `schema.sort` to `convertSortToQueryParams`, whose string arm is still present '
8746+ + 'at `sort-query.ts:66-70`. So this declaration lands AHEAD of the pinned consumer, '
8747+ + 'which the ruling permits explicitly (either order; the registrations already declare '
8748+ + 'the array). The in-repo sweep found ZERO authored `sort` on either block — the two '
8749+ + 'showcase pages that author `object-grid` (`command-center.page.ts`, '
8750+ + '`my-work.page.ts`) declare none — with the same grep shape finding 40+ string `sort` '
8751+ + 'values at OTHER doors (view definitions, ObjectQL `query.sort`) as the control that '
8752+ + 'the sweep fires; so this entry carries the prescription for authors outside the repo. '
8753+ + '⚠️ Metadata AT REST is deliberately NOT rewritten and this disposition adds no D2 '
8754+ + 'conversion: `os migrate meta --stored` replays D2 conversions only, and the read path '
8755+ + 'does not re-validate stored rows (`applyConversionsToStoredItem` replays the chain '
8756+ + 'without validating, by its own contract), so a stored page carrying a string `sort` '
8757+ + 'keeps loading and is still rendered by objectui at the pinned `.objectui-sha`. What '
8758+ + 'changes is that RE-SAVING it is refused at the `sort` door, on its next save and not '
8759+ + 'before. ADR-0049, ADR-0087.',
8760+ acceptanceCriteria:
8761+ '`ComponentPropsMap[\'object-grid\' | \'object-calendar\'].safeParse({ objectName, '
8762+ + 'sort: [{ field: \'created_at\', order: \'desc\' }] })` succeeds and the parsed `sort` '
8763+ + 'is that same array, equal value-for-value to '
8764+ + '`ElementDataSourceSchema.parse({ object, sort: <that array> }).sort`. The legacy '
8765+ + 'string clause is refused at the `sort` path on both doors (`invalid_type`, expected '
8766+ + 'array), and so is a bare number; a misspelled or ABSENT direction is refused at '
8767+ + '`sort.0.order` (`invalid_value` — `order` is a required enum, so both take one '
8768+ + 'verdict) and a missing field at `sort.0.field` (`invalid_type`). An undeclared key '
8769+ + 'is still refused BY NAME on the same call (`unrecognized_keys` naming it), the '
8770+ + 'control that makes those refusals verdicts rather than a schema reporting nothing. '
8771+ + 'No `sort` door in `ComponentPropsMap` accepts a string except `record:related_list`, '
8772+ + 'which is the one deliberate exception. At runtime each block orders exactly as the '
8773+ + 'array orders — the same `$orderby` the string lowered to.',
8774+ },
87058775 {
87068776 id: 'object-grid-data-view-data-converged',
87078777 surface:
0 commit comments