… (objectui#8961)
Executes the maintainer ruling of 2026-09-15, director batch #135 item 5,
letter A. Not a decision; execution.
## What a user saw
A view authored `sort: "name desc"` lit a descending arrow on the `name`
header before anyone clicked anything, while the query that fetched those
rows carried no ordering at all. The arrow stated something about the list
that was not true of the rows beside it, and the first click on that column
then asked for `asc` on a list that was in no declared order. The only
signal was a `console.error` no end user reads.
## Why the two halves disagreed
objectui#8221 retired the legacy string clause — one spelling, the array,
everywhere — and objectui#8767 (route C) made this block's fetch path
REFUSE a string `sort` and send no `$orderby`. That ruling deliberately
left the other reader of the same key alone: `parseSchemaSort`, which feeds
the header indicators, went on parsing `"name desc"` and
`["name desc", ...]`. One key, two readers, opposite answers.
## What this does
`parseSchemaSort` admits only `[{ field, order }, ...]` — the spelling
`@objectstack/spec` declares for `ObjectGridPropsSchema.sort` and the only
one the fetch path still lowers. A retired string yields nothing, so it
lights no arrow. The refusal is per entry: a mixed array still lights the
arrows for the keys spelled in the declared form.
No second diagnostic is added. The author is already told once per spelling
by PR #8758's reporter at the fetch path, which quotes the offending value
and prescribes the array form; a second voice here would only restore the
arrow the wire cannot honour.
## What is deliberately unchanged
The wire shape. The array arm still lowers to this block's own
`"field order"` join string; routing the key through the shared sink's
`{field: direction}` map is route B on objectui#8767, which stays declined
until the protocol declares that shape. Measured while here: the
server-side export path reads `schema.sort` itself rather than through
`parseSchemaSort`, so it was already array-only and does not move — the
retired docblock claim that narrowing this reader "takes the export path
with it" was not borne out.
## Pins, both directions
`serverSorting.test.tsx`: the declared array lights the arrow (the case
that already existed); a retired string lights NEITHER direction while the
query carries no `$orderby` and the one diagnostic still fires once. That
case previously pinned the divergence — the arrow drawn beside an empty
query — so the arrow half is what flips. Its non-vacuity control is the
neutral sort indicator, still rendered, so "no arrow" cannot pass by the
column having quietly stopped offering sorting. The unit block re-judges
every spelling it used to admit, each refusal paired with a declared-array
control.
`gridArrayArmOrderby-8973.test.tsx`'s docblock cited this card by a label
that had already moved; it now names the ruling, which does not move.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VCpmqvacV4BypY48QdoxcE
Fixes #8961
Executes the maintainer ruling of 2026-09-15 — director batch #135 item 5, 「135 同意」, letter A (objectui#8961 comment 5682605192). Not a decision; execution.
What a user saw
A view authored
sort: "name desc"lit a descending arrow on thenameheader before anyone clicked anything, while the query that fetched those rows carried no ordering at all. The arrow stated something about the list that was not true of the rows beside it, and the first click on that column then asked forascon a list that was in no declared order. The only signal was aconsole.errorno end user reads.Why the two halves disagreed
objectui#8221 retired the legacy string clause — one spelling, the array, everywhere. objectui#8767 (route C) made this block's fetch path REFUSE a string
sortand send no$orderby, and deliberately left the other reader of the same key alone:parseSchemaSort, which feeds the header indicators, went on parsing"name desc"and["name desc", ...]. One key, two readers, opposite answers.What this does
parseSchemaSortadmits only[{ field, order }, ...]— the spelling@objectstack/specdeclares forObjectGridPropsSchema.sort(read from the installed@objectstack/spec@17.4.0, not copied from the order) and the only one the fetch path still lowers. A retired string yields nothing, so it lights no arrow. The refusal is per entry: a mixed array still lights the arrows for the keys spelled in the declared form.No second diagnostic is added: the author is already told once per spelling by PR #8758's reporter at the fetch path, which quotes the offending value and prescribes the array form. A second voice here would only restore the arrow the wire cannot honour.
Route B remains declined and untouched — this diff routes nothing through
convertSortToQueryParamsand moves no wire shape.Pins, both directions
serverSorting.test.tsx— declared array lights the arrow (the case that already existed, unchanged); a retired string lights neither direction while the query carries no$orderbyand the one diagnostic still fires exactly once. That case previously pinned the divergence (arrow drawn beside an empty query), so the arrow half is what flips; the wire half stays asserted because "no arrow" is only right while the query really carries no ordering.gridArrayArmOrderby-8973.test.tsx's docblock cited this card by a label that had already moved twice; it now names the ruling, which does not move.Reverse verification (one-off, not left in the tree)
From the committed fix,
parseSchemaSort's narrowed body was reverted to its pre-fix form on disk and the pins re-run, then restored. Mutation and restoration were each proven on disk by occurrence counts, not by an editor's exit code. Numbers and the head they were taken at are in the report on objectui#8961.Gates and tests
Commands, exit codes and the ratchet-bearing runs are listed in the report comment on objectui#8961. Repo-wide scans (
pnpm lint, the fullpnpm test) are CI's run, not this branch's claim.Measured while here, out of scope
The docblock this diff replaces claimed that narrowing this reader "takes the export path with it". It does not: the server-side export path reads
schema.sortitself (Array.isArray(schemaSort)), never throughparseSchemaSort, so it was already array-only and does not move. The new docblock says so.Acceptance notes
plugin-view'sObjectView.tsxcarries a cross-package comment quoting this function's old opening line verbatim (typeof sort === 'string' ? [sort] : ...). Its load-bearing conclusion — a bare object yields an empty list — is still true; the quoted opening is not. Left untouched: that file is outside this claim's declared surface. Noted, not filed; the successor is whoever next edits that sort block.sortarray rather than omitting the key (the entries are filtered out, the empty array is still sent). Pre-existing, unrelated to the arrow, not filed.Generated by Claude Code