Repository navigation
finding(plugin-grid): object-grid lowers schema.sort with its OWN private code, so a bare grid still honours the retired string clause that object-view now refuses — the per-block divergence the #8221 ruling rejected by name #8767
Description
Activity
- addedbugSomething isn't workingSomething isn't workingdomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec laneobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec lanedomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Sep 9, 2026 ⛔ CORRECTION — half of this card's framing is FALSIFIED, and the real divergence is worse than filed
domain:spec@objectuiPM seat, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w, 2026-09-09T05:05Z. ⛔ The body above is not edited; the invalidation sits beside it (「事后证伪同处公开作废」).What this card claimed
After #8758 lands, a bare
object-gridstill honours the retired spelling at runtime, while the same key routed throughobject-viewis refused with a diagnostic.What was measured
The second half is false. The ceiling-tier contract review of PR #8758 (objectui#8221 comment
5595976918, tier-verified, VERDICT PASS) measured it through the realObjectGrid:object-view's default view type is'grid'(ObjectView.tsx:802), and the grid path forwardstable.sortverbatim.- Readings: grid-default →
{"orderby": "name desc", retiredMsgs: 0}; grid-explicit → the same; calendar-string and kanban-string → no$orderby, 1 diagnostic each. ListView.parseSortConfiglikewise still parses the retired clause.
⇒ "Refused through
object-view" holds only for NON-GRID view types. Throughobject-view's default path the retired spelling is still honoured, silently — zero diagnostics.Why this makes the card more urgent, not less
I filed this as "one key, two meanings, chosen by which block you are on". The measured shape is sharper and worse:
- The divergence is not grid-vs-view; it is the default path versus everything else. The configuration an author is most likely to have —
object-viewwith no explicit view type — is the one that silently keeps the retired behaviour. - It is silent. The diagnostic feat(core)!: retire the legacy string
sortclause — one spelling, the array (objectui#8221) #8758 ships does not fire on that path, so nothing tells an author their document is relying on a retired spelling.
⚠️ A fourth declared face the PR body did not name also moved by derivation, per the same review:ObjectViewSchema.table.sort(TSPick<ObjectGridSchema, ObjectGridSlotKey>, zodObjectGridSchema.omit().partial()) — string now REFUSEDtable.sort/invalid_type, array parses. So the declared face for the grid slot IS narrowed; only the runtime lowering is not. That gap — declared-narrow, runtime-tolerant — is exactly the declared-vs-enforced split #8221 exists to close, reproduced one layer down.What does NOT change
- ⭐ The review's verdict on PR feat(core)!: retire the legacy string
sortclause — one spelling, the array (objectui#8221) #8758 is PASS, and its judgement is that this asymmetry is correctly a separate card, not incoherence of that PR — the ruling names the helper, its signature and the docs, ⛔ notObjectGrid's private lowering, and closing it rewrites ≥4 pinned wire-shape tests. - The three routes A/B/C in the body stand, and C is still the recommendation.
⚠️ But re-scope C: the refusal must cover theobject-viewdefault grid path andListView.parseSortConfig, ⛔ not only a bareobject-gridnode. - The ordering constraint stands: ⛔ not actionable until PR feat(core)!: retire the legacy string
sortclause — one spelling, the array (objectui#8221) #8758 lands.
⛔ Re-derive all of the above before implementing — these are the reviewer's readings, ⛔ not this seat's, and this card's own body has now been wrong once.
Generated by Claude Code
⛔ CORRECTION before this is ruled — the divergence is WIDER than this card states, and the difference changes what route A costs
domain:spec@ objectui seat, sessionsession_012W3vMLTFY9SPr2LyxhSeYi. Reading taken 2026-09-09T05:2xZ. ⛔ Not claiming, labels untouched.⚠️ Provenance, stated as plainly as this card states its own: the measurement below is the ceiling-tier (CONTRACT_REVIEW_TIER) reviewer's of PR #8758, tier-verified (134 transcript stamps, zero residue) and recorded at objectui#8221 comment5595976918. ⛔ I have not independently re-derived it either.The sentence that needs correcting
This card's title and body both say the retired spelling survives on a bare grid "while the same key routed through
object-viewis refused".Measured through the real
ObjectGrid, that is false:path $orderbyproducedretirement diagnostics object-view, default view type"name desc"0 object-view, explicit grid"name desc"0 object-view+ array arm"name desc"(comma-joined)0 object-view, calendarnone 1 object-view, kanbannone 1 ObjectView.tsx:802makes'grid'the default view type, and the grid path forwardstable.sortverbatim (already pinned byObjectView.canonicalTableKeys.test.tsx). ⇒object-viewrefuses the clause only in its non-grid view types.ListView.parseSortConfiglikewise still parses the string.Why this matters to the decision this card is asking for
⭐ It makes route A ("leave it") worse than the card prices it. A is argued on the basis that the grid is one isolated block whose string handling becomes dead tolerance. But the honest scope is the whole grid delegation path —
object-grid,object-viewin its default mode, andparseSchemaSort— i.e. the most-travelled route through this component, not a corner. "One key, two meanings, chosen by which block you are on" is真 even insideobject-viewalone, depending on its view type.⇒ Suggested scope amendment if C is ruled: the refusal belongs at the read site that serves all three, and the pin should cover
object-viewin grid mode explicitly, since that is the path a reader of this card would currently assume is already safe.⛔ Nothing here disputes the card's routes, its recommendation of C, its appetite, or its ordering note (only actionable after PR #8758 lands). Those all stand and I endorse them.
Duplicate disclosure — mine, and I am closing it
I filed objectui#8764 for this same defect at ~05:0xZ, ⛔ not having seen this card. My dedupe returned 14 substantive results with the right neighbours (#5270, #6235, #6011) and missed this one, filed 20 minutes earlier — a live-instrument search that still missed, which is worth recording as exactly the failure mode a control does not protect against. #8764 is closed as
duplicateof this card; this one is earlier, better structured, and already routed to the decision box. Everything #8764 carried that this card did not is the correction above.
Generated by Claude Code
⭐ RULING: route C —
ObjectGridREFUSES the string arm. Maintainer, 2026-09-10.Keep the wire shape; make
ObjectGridrefuse a stringschema.sortwith the same diagnostic, before its own lowering. ⛔ Route B (route through the shared sink) is not taken — it moves the wire shape for every grid and drags in the export path andparseSchemaSort; that is a different card. ⛔ Route A (leave it) is refused: it is exactly the per-block divergence the #8221 ruling declined by name. ⛔ The direction is not re-opened by the implementing seat.Claim: session
session_01Jmxdo7bmeqCQHLSfmLVX9w(PM seatdomain:spec, dispatchingos-dev) · branchclaude/issue-8767-object-grid-refuses-string-sort
Clause-②: yes — a string that lowers today stops lowering; non-additive on a published face, same shape as #8221.Assignee set,
pm:dispatchedadded. ⛔ No secondClaim:, ⛔ never write the assignee.The ordering precondition is now MET — measured, not assumed
This card said "⛔ Do not dispatch it before PR #8758 lands." It has landed, and that is established by commit and ancestry, ⛔ not by an API field:
git log origin/main --oneline --grep="(#8758)" → rc 0, 1 match git merge-base --is-ancestor 9a853f2f3… origin/main → rc 0 (is an ancestor)Squash
9a853f2f3abc924d62dacb2962e515283aad4bb2— "feat(core)!: retire the legacy string 'sort' clause — one spelling, the array (objectui#8221) (#8758)", 25 files. ⭐packages/plugin-grid/is not among the 25.The divergence, re-derived on
origin/main452f1f779convertSortToQueryParams(packages/core/src/utils/sort-query.ts:132) post-#8758 refuses a string at:141-144viareportRetiredSortSpelling(:110-122, an unconditionalconsole.error) and returnsundefined, so the query carries no$orderby.packages/plugin-grid/src/ObjectGrid.tsx:1465takesconst schemaSort = schema.sort;and:1850-1863lowers it privately:} else if (schemaSort) { if (typeof schemaSort === 'string') { params.$orderby = schemaSort; } else if (Array.isArray(schemaSort)) { params.$orderby = schemaSort.map((s: any) => `${s.field} ${s.order}`).join(', '); } }FIRING CONTROL, same command:
convertSortToQueryParams= 107 hits repo-wide; per-package non-test undersrc/it lights up seven sibling packages —plugin-calendar3 (ObjectCalendar.tsx:520),plugin-gantt3,plugin-map3,plugin-timeline3,plugin-view5,plugin-form3,app-shell3 — andplugin-grid= 0, absent fromObjectGrid.tsx:39's@object-ui/coreimport list. ⇒ "ObjectGrid is not among them" is a reading.⚠️ The divergence is SHARPER than this card states — read this before scopingThe shared sink now emits a
{field: direction}map.ObjectGrid:1854forwards a string verbatim, and:1856-1858lowers the array form into a"field order, field order"join string — i.e. it manufactures the very spelling #8758 retired. There is also a second private lowering:ObjectGrid.tsx:71-72'sparseSchemaSort(used at:4029-4030, exported, pinned at__tests__/serverSorting.test.tsx:185-210), whose docblock at:60-64still asserts the pre-#8758 world.⛔ None of that is in scope. The wire shape is route B. What C changes is only: a string arm that lowers today must instead be refused with #8758's diagnostic. The array path, the join-string output,
parseSchemaSortand the export path are ⛔ untouched.⚠️ Two live pins currently assert the join-string output —__tests__/ObjectGrid.elementDataSource.test.tsx:75andgridDefaultFiltersLowering.test.tsx:275,286, bothexpect(params.$orderby).toBe('name desc'). Read them end to end. Under route C they should stay green (they author the array form). If either authors a string, that is the card's own subject and it flips to asserting the refusal — ⛔ say which, with the reading, rather than editing quietly.Appetite, as the card scoped it
In scope: the refusal + diagnostic at
ObjectGrid's own read site, reusing #8758's reporter (deduped once per spelling), and a pin that a bareobject-gridrefuses the clause while the array arm lowers unchanged.⛔ Falls off the back: the wire shape (route B); the export path and
parseSchemaSort; anything aboutobject-calendar, whose local face declaressort?: anyand is wider than the retired union anyway (#8171 and the ruling's item 4 own that block).Ablation: prove the refusal fires on a string and that the array arm still lowers to the same
$orderbyit does today. Prove every mutation on disk before reading any result (anchor counts on injected and removed text, plus agit hash-objectdiffering from the HEAD blob), restore from atrap … EXIT INT TERM, prove restoration by blob hash — ⛔ never by an exit code.A changeset. ⛔ Never edit
content/docs/releases/. BareFixes #8767— ⛔ not the cross-repo form.⚠️ Provenance note this card carries and this dispatch keeps: the original measurement was theos-devseat's, taken while implementing #8221. ⛔ Re-derive on your own head — everything quoted above was re-derived by this seat on452f1f779, but the tree moves.Standing rules
⛔ Worktree-first —
⚠️ /home/user/objectuiis shallow with a staleorigin/main; ⛔ nevergit fetch/git checkoutthere. Clone fresh. ⛔ Nevergit stash. ⛔ Never kill by name. ⛔ Never mutatenode_modules. ⭐pnpm --filter @object-ui/plugin-grid lint→ exit 0, read for the literal worderror. ⛔ PR stays draft.
Generated by Claude Code
os-dev-report
{ "issue": 8767, "status": "done", "branch": "claude/issue-8767-object-grid-refuses-string-sort", "pr": "https://github.com/objectstack-ai/objectui/pull/8960", "premise_still_valid": true, "summary": "Route C landed exactly as ruled: ObjectGrid's private string arm no longer assigns $orderby, it calls the shared sink convertSortToQueryParams for its refusal alone, so PR #8758's own reporter names the retired spelling once per spelling and the query carries no ordering. The array arm, the join-string wire shape, the export path and parseSchemaSort are byte-for-byte untouched. A new pin (gridRetiredStringSort-8767.test.tsx) asserts BOTH halves so it cannot pass by refusing everything, and a changeset declares the minor bump. MECHANISM CORRECTION worth the PM's attention: reportRetiredSortSpelling is module-PRIVATE to packages/core/src/utils/sort-query.ts and is exported nowhere, so 'reuse #8758's reporter' and 'do not change sort-query.ts' are only jointly satisfiable by calling the exported sink itself on the string arm and discarding its (always undefined) return; that is what landed, and no second reporter was written.", "tests": "All exit codes captured into files BEFORE any pipe; heavy runs serialized through scripts/pm/os-verify-lock.sh and read from its VERDICT line, never a bare $?. (1) pnpm exec vitest run packages/plugin-grid/ -> exit 0, 'Test Files 121 passed (121)', 'Tests 1070 passed (1070)'. (2) the four sort files on the final restored tree -> exit 0, 'Test Files 4 passed (4)', 'Tests 36 passed (36)'. (3) cross-package readers of ObjectGrid.tsx plus core sort-query.test.ts and plugin-view ObjectView.sortSink.test.tsx -> exit 0, 'Test Files 6 passed (6)', 'Tests 50 passed (50)'. (4) turbo run type-check --filter=@object-ui/plugin-grid --concurrency=2 -> exit 0, 14 tasks successful, log echoes '> @object-ui/plugin-grid@17.6.0 type-check' and '> tsc --noEmit && tsc -p tsconfig.test.json'. NOTE: a first bare 'pnpm --filter @object-ui/plugin-grid type-check' exited 2 with 'Cannot find module @object-ui/core' -- dependency dist not built, i.e. PREREQUISITE NOT MET, recorded as such and NOT counted as a failed measurement; the turbo run is the reading. (5) LINT READING: pnpm --filter @object-ui/plugin-grid lint -> exit 0; log echoes '> @object-ui/plugin-grid@17.6.0 lint' and '> eslint .'; summary line '794 problems (0 errors, 794 warnings)'; ZERO lines matching the eslint severity-error shape and ZERO occurrences of the literal lowercase word 'error' anywhere in the 382KB log (the only 'Error' tokens are capital-E inside a react-hooks warning message). (6) node scripts/check-changeset-presence.mjs -> exit 0, 1 changeset declared for 2 changed published source files. (7) pnpm changeset:check -> exit 0, fixed group OK, no major. (8) pnpm check:control-bytes -> exit 0, 7166 tracked text files. (9) node scripts/check-governed-queue-guard.mjs --test on all four changed paths -> exit 0, 'NOT GOVERNED'. ABLATION (resolution path stated first: plugin-grid tests import ../ObjectGrid relatively and the root vitest config aliases every @object-ui/* specifier to that package's src, so nothing resolves through dist and no build gates these legs). HEAD blob of ObjectGrid.tsx = 064b293dcd210d1d62622bc8aff059b59489d80e. Each leg proved ON DISK BEFORE any result was read (grep count on the INJECTED marker = 1 AND on the REMOVED literal = 0, plus git hash-object differing from the HEAD blob), restored from a trap on EXIT INT TERM with absolute paths via 'git checkout HEAD -- PATH', restoration proven BY BLOB HASH and by an empty 'git diff HEAD', never by an exit code. LEG 1 (delete the refusal, restore params.$orderby = schemaSort): mutated blob fe6a396fb4b0f1abb2c9c017e23531edf791839c; predicted before the run = 4 refusal cases RED, 2 array CONTROL cases GREEN, other three files GREEN; observed exactly that -- 'Test Files 1 failed | 3 passed (4)', 'Tests 4 failed | 32 passed (36)'; restore hash 064b293d... == HEAD. LEG 2 (mutate the ARRAY arm's template from field-space-order to field-colon-order; a separator-only mutation would have been invisible on a single-key sort): mutated blob 930650a794ab2d40e8ef08444b52bd31b1fb275f; predicted = the 2 array CONTROL cases RED, elementDataSource:75 RED, gridDefaultFiltersLowering:286 RED, the re-authored serverSorting case RED, the 4 refusal cases GREEN, and -- the asymmetry that makes it a control -- gridDefaultFiltersLowering:275 (the legacy defaultSort leg, a different code path) GREEN; observed exactly that -- 'Test Files 4 failed (4)', 'Tests 5 failed | 31 passed (36)'; restore hash 064b293d... == HEAD. Leg 1 says the refusal is what makes the pin pass; leg 2 says the array assertions really measure the join output, so 'the array arm still produces the same $orderby it does today' is a measurement rather than a vacuous claim. RE-DERIVED MEASUREMENTS on my own head 72bcd7783 (fresh full clone, git rev-parse --is-shallow-repository = false): git log origin/main --grep='(#8758)' -> rc 0, 1 match 9a853f2f3; git merge-base --is-ancestor 9a853f2f3 origin/main -> rc 0, and the CONTROL leg (the repo root commit against the same target ref) -> rc 0, so the positive ancestry reading stands without a shallow-window caveat. convertSortToQueryParams refuses a string at packages/core/src/utils/sort-query.ts:141-144 via reportRetiredSortSpelling at :110-122 (unconditional console.error, deduped per spelling) and returns undefined. ObjectGrid.tsx:1465 'const schemaSort = schema.sort;' confirmed; the private lowering is at :1852-1859 on my head (the dispatch quoted :1850-1863 on 452f1f779 -- same code, shifted). FIRING CONTROL reproduced: convertSortToQueryParams lights up seven sibling packages non-test under src/ (plugin-calendar, plugin-gantt, plugin-map, plugin-timeline, plugin-view, plugin-form, app-shell) and plugin-grid = 0, absent from ObjectGrid.tsx:39's @object-ui/core import list. NEW HEAD SHA: 76a9ead402cc173ce7c9bea00eaf6daed59c2bb9.", "mcp_calls": "2 - one mcp github search_issues (the single de-dup search, after the repo-scoped REST /search/issues endpoint answered HTTP 403 'sessions are bound to their configured repositories'; channel switch declared here) and one mcp github create_pull_request. Every other GitHub read and write went through repo-scoped REST curl: the card body, the claim comment, the label probe, the label write, the PR read-back, the new card, this comment.", "open_questions": [], "out_of_scope_findings": [ "THE JOIN-STRING PINS, read end to end. Neither pin the dispatch named authors a string, so both stay green untouched: ObjectGrid.elementDataSource.test.tsx:75 authors HOT_VIEW.sort = [{ field: 'name', order: 'desc' }] (ARRAY), and gridDefaultFiltersLowering.test.tsx:286 authors sort: [{ field: 'name', order: 'desc' }] (ARRAY) while :275 is the legacy defaultSort leg, not the sort key at all. A THIRD live pin the dispatch did not name DOES author a string and is this card's own subject: serverSorting.test.tsx:141, renderGrid(ds, { sort: 'name desc' }) asserting $orderby toBe('name desc') under the comment 'The declared sort goes out as the string form it was authored in'. It would have gone red. Its test name is 'replaces the view's declared sort rather than stacking on it' -- its subject is objectui#3106's header click and the string spelling was incidental -- so its fixture is re-authored in the declared ARRAY spelling (assertion value unchanged, because the array arm is unchanged), keeping its #3106 subject alive, and the refusal it used to contradict is pinned separately with both halves. Saying which, with the reading, rather than editing quietly.", "filed as #8961: after this PR the header-arrow reader parseSchemaSort still parses the retired string spellings the fetch path now refuses, so a grid authored sort: 'name desc' shows a descending arrow while its query carries no ordering at all. Reproducible, and the probe is already in the tree and GREEN: serverSorting.test.tsx's 'shows the view's declared sort before anyone clicks' case, which authors sort: 'status desc' and which I deliberately left authoring the retired spelling (with an in-place annotation) rather than converting it, so the divergence stays visible instead of being papered over. Filed unassigned, no labels, cross-linked to #8767 / PR #8960 / #8221 / PR #8758. De-dup done first: one targeted search whose own control was satisfied in-call (it returned #8767 itself plus the closed predecessor #8764 and #8221); no open card covered it.", "noted, not filed: two comments in ObjectGrid.tsx that THIS change falsifies were corrected, comment-only, no behaviour: the parseSchemaSort docblock had claimed 'this grid's own fetch path already reads all three' spellings, and the declaredSort read site at :4019 had claimed 'the arrow on screen and the $orderby on the wire are the same sort'. Both now name the one declared spelling and record that this reader is wider than the fetch path. Flagged loudly because the dispatch lists parseSchemaSort as out of scope -- the edits touch neither its behaviour nor its exported signature, only claims my own change made untrue. Successor: #8961.", "noted, not filed: ObjectGrid's array arm MANUFACTURES the very 'field order, field order' join string #8221 retired, where every sibling block now sends the shared sink's field-to-direction map. Route C keeps it deliberately and this PR does not touch it. Successor: #8961, which asks the wire shape and the header reader as one coupled question, because narrowing either alone leaves the other half divergent.", "noted, not filed: serverSorting.test.tsx's 'parseSchemaSort -- the header reads what the fetch path reads' unit block (its bare-string, array-of-strings and default-direction cases) still pins the retired spellings as accepted INPUTS to that reader, and its describe title is now false of the fetch path. Left entirely untouched -- it is that reader's own contract, which the ruling parked. Successor: #8961.", "noted, not filed: node scripts/check-changeset-presence.mjs reported '2 file(s) changed' because the new test file and the changeset were still untracked when it ran; it re-ran green after the commit as part of check:control-bytes' tracked-file count moving 7164 -> 7166. Not a defect in the gate, recorded so the two numbers in the log are not read as a disagreement. Successor: none." ] }
Generated by Claude Code
Contract review handoff — FAIL, carriers stripped, repair round in flight
PM seat (
domain:spec, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w), 2026-09-10.Review record:
5618090408on PR #8960 —## Contract review, judged head76a9ead402cc173ce7c9bea00eaf6daed59c2bb9(base72bcd7783),VERDICT: REWORK.Independence pair:
Implemented-by: claude/issue-8767-object-grid-refuses-string-sort (mode:subagent) Reviewed-by: agent-a6b18b9bebde4b22eBoth are subagents of the one dispatching session ⇒ the record declares SELF-REVIEW. ⛔ Not an independent second opinion; recorded as such and acted on in the stricter direction.
What is owed (the repair round is dispatched on the same branch; PR stays open and
draft):- R1(a) — the changeset sentence "the only one
@objectstack/specaccepts" is false.ObjectGridPropsSchema.sortisz.unknown(), so the protocol's validator parses the string,42, string arrays and objects alike. The true sentence is about what the protocol declares, not what it accepts. - R1(b) — "
orderis optional and means'asc'" is false:SortItemSchema.order(objectstackpackages/spec/src/shared/enums.zod.ts:27-30) carries no.optional(). The untouched array arm emits$orderby: 'name undefined'when it is omitted — ⛔ out of scope for this PR, owed a successor card beside objectui#8961. - R2 —
serverSorting.test.tsx:170still authors the retired string, green, while the wire carries no ordering: a frozen defect by the implementer's own:141principle. Re-author in the array form, pin the arrow/wire divergence explicitly for objectui#8961, and fix the now-falsedescribetitle at:185.
⛔ Not reopened: the refusal mechanism, its control pins, the
minorgrading, and objectui#8961's parking — the record judged all four correct.Route C is not in question. Measured in this seat on
objectstack-ai/objectstackHEADeabdd66f45f402eba0f8404a8a9de4a501fc83a6:ui/component.zod.ts:2470describesobject-grid.sortas "Initial sort (array of{ field, order })",:2489-2498'sdefaultSortretirement names the live mechanism as "the same pair, wrapped in an array", and:2231shows the protocol's typed spelling of the same intent on a sibling block (z.array(SortItemSchema)).z.unknown()is the protocol not policing the field, ⛔ not the protocol blessing the string — so route C moves toward the protocol and the «以 objectstack 协议为准» principle does not bite.⛔ Carrier correction — this supersedes what I wrote on the PR an hour ago
On PR #8960 I said
needs:contract-reviewstays hung on both carriers through a REWORK. That was wrong, and it was wrong because I was reading a stale copy of the protocol:.claude/skills/pm-dispatch/references/contract-review.mdnow readsFAIL 同 PASS 剥双载体:同笔留卡上交接评论(引复审、独立性对、欠改);卡态与 assignee 不动。
⇒ a FAIL strips both carriers exactly as a PASS does, with this comment as the handoff. Both are stripped in the same stroke. The card's state (
pm:dispatched) and assignee do not move; the repair round re-hangs the label when it delivers.I had also hung the label on this card at 11:4xZ to repair a 漏挂 the review caught. That hang was correct at the time and is now superseded by the strip — the record exists, so the carrier is no longer 待审.
Generated by Claude Code
- R1(a) — the changeset sentence "the only one
os-dev-report
{ "issue": 8767, "status": "done", "branch": "claude/issue-8767-object-grid-refuses-string-sort", "pr": "https://github.com/objectstack-ai/objectui/pull/8960", "premise_still_valid": true, "summary": "REWORK round, both items landed at head 52d28eb4b; the reviewed mechanism is untouched -- git diff --name-only 76a9ead40..HEAD names only the changeset and serverSorting.test.tsx, so ObjectGrid.tsx is byte-identical and the refusal, the control pins, the minor grading and objectui#8961's parking are exactly as judged. R1: both false contract sentences in the changeset are replaced with re-derived truths -- the spec's grid props face is value-agnostic (z.unknown()) and PARSES the string, so this change is NOT backed by the validator; what the protocol declares is the array, in three independent places; and SortConfig.order is required on both objectui faces and in the protocol's SortItemSchema, while this block's untouched array arm emits $orderby 'name undefined' when it is omitted. R2: the arrow case's fixture moves to the declared array spelling, the string's behaviour becomes an explicit pin naming objectui#8961 that asserts BOTH halves (arrow drawn AND $orderby key absent AND one diagnostic) instead of a green test that only asserted the arrow, and the falsified parseSchemaSort describe title is renamed. The array-arm undefined defect is carried by objectui#8973 with a measured probe table, not widened into this diff.", "successor_card_opened_this_round": 8973, "tests": "All exit codes captured into files BEFORE any pipe; heavy runs through scripts/pm/os-verify-lock.sh, read from its VERDICT line. THIS ROUND, on head 52d28eb4b: (1) the five sort-affected plugin-grid test files -> exit 0, 'Test Files 5 passed (5)', 'Tests 45 passed (45)'. (2) turbo run type-check --filter=@object-ui/plugin-grid --concurrency=2 -> exit 0, 14 tasks successful. (3) pnpm --filter @object-ui/plugin-grid lint -> exit 0; log echoes '> @object-ui/plugin-grid@17.6.0 lint' and '> eslint .'; summary '794 problems (0 errors, 794 warnings)'; ZERO eslint severity-error lines and ZERO occurrences of the literal lowercase word 'error' in the whole log. (4) node scripts/check-changeset-presence.mjs -> exit 0, 3 published source files changed, 1 changeset declared. (5) pnpm changeset:check -> exit 0. (6) pnpm check:control-bytes -> exit 0, 7166 tracked text files; plus a manual control-byte scan of both edited files, no hits. (7) PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 8960 -> exit 0, 'both carriers agree' (run from the /home/user/objectstack checkout, which is at eabdd66f45f and is behind origin/main -- the script version is therefore possibly stale, and the exit code is reported as read). R1 RE-DERIVATION, measured by me rather than taken from the review, against the installed @objectstack/spec@17.4.0 via ui.ObjectGridPropsSchema.safeParse: 'name desc' PARSES, 'name' PARSES, 42 PARSES, ['name desc'] PARSES, {name:'desc'} PARSES, [{field:'name',order:'desc'}] PARSES, and the CONTROL bogusProp is REFUSED with unrecognized_keys -- the control is what makes those six a reading rather than a schema that accepts anything. Cause read on the protocol source: ObjectGridPropsSchema.sort is z.unknown().optional() with describe 'Initial sort (array of { field, order })'; the sibling ElementRecordPickerPropsSchema.sort is z.array(SortItemSchema).optional(), measured to PARSE the declared array, REFUSE 'name desc' (invalid_type at root) and REFUSE ['name desc'] (invalid_type at 0); SortItemSchema requires order, measured -- it REFUSES [{field:'name'}] with invalid_value at 0.order; and the defaultSort retirement text instructs authors to wrap the value in an array. objectui side: SortConfig.order has no '?' in packages/types/src/objectql.ts and no .optional() in packages/types/src/zod/objectql.zod.ts. ARRAY-ARM PROBE for objectui#8973 -- rendered the real object-grid through SchemaRenderer against a vi.fn() data source and read $orderby off the first find call, as a ONE-OFF temporary test file created and removed inside the same script under a trap on EXIT INT TERM; removal confirmed by the tree being clean of it. CONTROL [{field:'name',order:'desc'}] -> 'name desc'; [{field:'name'}] -> 'name undefined'; [{field:'name',order:'desc'},{field:'status'}] -> 'name desc, status undefined'; [] -> '' (an empty $orderby is sent rather than the key omitted); ['name desc'] -> 'undefined undefined'; [{order:'desc'}] -> 'undefined desc'. The control row is what makes the other five a measurement. DECLARED NARROWING, this round only: the full 121-file plugin-grid suite was NOT re-run locally on 52d28eb4b. The shared verify lock returned exit 99 (never acquired, NOT MEASURED) TWICE, 9 minutes of budget each, against one holder -- pid 6809, bash .../issue-8934/r2-verify.sh -- which held it continuously for 23 minutes; my slot dev-8767 stayed parked so the ticket kept its arrival stamp, and the third attempt acquired in 62s and held 179s with a narrowed set. The narrowing is sound, not merely convenient: the only source file this round changed is serverSorting.test.tsx, which nothing imports, plus a .changeset/*.md; ObjectGrid.tsx is byte-identical to 76a9ead40, where the full suite was green at 'Test Files 121 passed (121)' / 'Tests 1070 passed (1070)'. CI runs the whole farm on the pushed head. PRIOR ROUND readings (head 76a9ead40, unchanged and still standing): full plugin-grid suite 121 files / 1070 tests exit 0; cross-package readers 6 files / 50 tests exit 0; both ablation legs with on-disk proofs and blob hashes (HEAD blob 064b293dcd210d1d62622bc8aff059b59489d80e; leg-1 mutated blob fe6a396fb4b0f1abb2c9c017e23531edf791839c, leg-2 930650a794ab2d40e8ef08444b52bd31b1fb275f; both restored to the HEAD blob with an empty git diff HEAD). NEW HEAD SHA: 52d28eb4b4551fffe3ffc9a46fde2d532fc99785.", "mcp_calls": "0 this round; 2 cumulative for the card (both in round 1: one search_issues for the de-dup, one create_pull_request). Everything in this rework round went through repo-scoped REST curl: reading the two review comments, the PR body read and three PATCH updates, filing objectui#8973, hanging both labels, posting this comment.", "carrier_state": "needs:contract-review is hung on BOTH carriers again, in one stroke, additively (POST .../labels, never a whole-set PUT), and read back contrastively: card objectui#8767 went from [bug, pm:dispatched, finding, domain:spec, domain:ui] to that set plus the gate, PR #8960 from [plugin, tests] to that set plus the gate -- union(before, target) equals the read-back on both, so no concurrent labeler stripped anything. My judgement for hanging it: the repaired diff still carries the same Clause-2 narrowing the ruling declared, because ObjectGrid.tsx did not move this round. check-clause2-carriers --pair 8960 exits 0.", "open_questions": [], "out_of_scope_findings": [ "filed as objectui#8973 (NEW, this round): object-grid's private array arm interpolates every key unconditionally, so a SortConfig with order omitted goes out as $orderby 'name undefined'; the full six-row probe table with its control is in the card. Filed unassigned, ungraded, no labels, same technique as objectui#8961. It matters more after this PR than before it, because the diagnostic this PR makes the grid print says verbatim that order is optional and means 'asc' -- true of the shared sink, false of this block -- so the grid now prescribes a migration that produces garbage on the grid. NOT repaired here: widening the diff to the array arm is the direction the ruling refused. The contract review's own suggestion was to append these readings to objectui#8961 rather than open a separate card; objectui#8973 says so in its own body, so triage may fold it in and mark it a duplicate.", "R2 SELF-CORRECTION, worth stating plainly: my first round left the arrow case authoring the retired string on the reasoning that this kept the divergence visible, and reported that as a deliberate choice. The review overturned it and is right by my own principle for the sibling case -- a green test does not make a state visible, it certifies it as expected. It is now an explicit two-sided pin naming objectui#8961, and that case is the one that has to change when objectui#8961 closes the gap.", "filed as objectui#8961 (round 1, unchanged): the header-arrow reader parseSchemaSort still parses the retired string spellings the fetch path now refuses, so a grid authored with a string shows an arrow for an ordering its query does not carry. objectui#8973 and objectui#8961 are two halves of one question -- object-grid still owns two private lowerings of a key every sibling block routes through the shared sink -- and objectui#8973 says so.", "noted, not filed: THREE GitHub body-mutation behaviours hit this round and were handled rather than reported as damage. (i) Every PR-body PATCH appends a second attribution footer unconditionally: measured at exactly +58 bytes on each of three PATCHes, with a unified diff showing an ADD and nothing eaten. Handled by stripping every trailing footer from the body I send, so exactly one comes back. (ii) A PR body created with the session-URL footer loses the session reference on its first edit. Handled with the prescribed mitigation: the session URL is now in the body PROSE as a backtick code span, which survives the edit path. (iii) I caught a closing-keyword hazard in my own updated body before it could do harm -- a sentence saying the array-arm defect is not addressed in this PR and is carried by the successor card had placed a closing keyword a few words ahead of that card's number, and the merge parser matches keyword-plus-reference without reading negation, so on merge it could have silently shut the successor card. The sentence is rewritten so no closing keyword sits ahead of any number other than the one this PR is meant to close, and a scan of the stored body now shows exactly one such hit, the intended one on this card. The same scan over both successor cards is clean. Successor: none, these are documented repo behaviours.", "noted, not filed: the array-arm outputs the probe measured for [] ('' rather than an omitted key), ['name desc'] ('undefined undefined') and [{order:'desc'}] ('undefined desc') are the same defect class as objectui#8973's headline and are carried in that card's table rather than split across cards. Successor: objectui#8973.", "noted, not filed: serverSorting.test.tsx's parseSchemaSort unit block still admits the retired spellings as INPUTS to that reader; the block is untouched and its describe title now says what it actually pins. Re-judging those inputs is objectui#8961's. Successor: objectui#8961." ] }
Generated by Claude Code
Clause-② carriers cleared — PASS on record
PM seat (
domain:spec@ objectui, sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w), 2026-09-10.Review record:
5619296255—## Contract review, round 2 (delta),VERDICT: PASS, judged head52d28eb4b4551fffe3ffc9a46fde2d532fc99785. The head moved after the round-1 REWORK at5618090408, so that record no longer counts; this one is the record on the current head.Independence pair:
Implemented-by: claude/issue-8767-object-grid-refuses-string-sort (mode:subagent) Reviewed-by: session_01Jmxdo7bmeqCQHLSfmLVX9w (contract-review subagent agent-a6b18b9bebde4b22e, context-isolated)SELF-REVIEW — implementer and reviewer are both subagents of the one dispatching session. ⛔ Recorded as such, not as an independent clearance.
Landing pre-checks, all three:
- ① in-seat clause-② PASS on record — the record above, on this head. ✅
- ②
PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 8960→ rc 0, run with the current script (objectstackorigin/main776d64cd3, which #17349 taught to read the record on the head). ✅ - ③ 33 check runs: 30 success, 3 skipped, 0 failing, 0 pending on
52d28eb4b— every check, ⛔ not a required subset. ✅
needs:contract-reviewis stripped from both carriers in the same stroke as this comment.Both round-1 items landed
- R1 — the two false contract sentences are gone from the changeset. The review found no false contract sentence remaining. ⭐ The distinction that mattered: the protocol's
ObjectGridPropsSchema.sortisz.unknown(), so it parses a string; what the protocol declares is the array of{ field, order }. A sentence about the validator and a sentence about the declaration are not interchangeable. - R2 —
serverSorting.test.tsxre-authored, the arrow/wire divergence now pinned explicitly (both halves asserted, not just the arrow), and the falsedescribetitle fixed. The dev's self-correction was verified by ablation. - The
$orderby: 'name undefined'defect in the untouched array arm was filed as objectui#8973, as asked. ⛔ Not folded into this PR.
Two nits carried, ⛔ neither blocking and neither in this diff
The review named them for the dispatch seat rather than as REWORK items, and I am recording rather than acting on them:
packages/types/src/objectql.ts:695— theObjectGridSchema.sortdocblock still reads "orderis optional and means'asc'" two lines above a declaration whereorderis required;sort-query.ts:42carries the same sentence. Both are objectui#8758's text. ⇒ they belong on objectui#8973 beside the diagnostic's wording, not here.- The changeset says "filed as a successor card" without a number, while the PR body names finding(plugin-grid):
object-grid's private array-arm lowering interpolates missing keys into$orderby— aSortConfigwithorderomitted goes out asname undefined#8973. Traceability only.
⚠️ Not measuredRead from the review rather than re-run by this seat: the full
plugin-gridsuite locally,type-checkandlintlocally (CI only, green on this head), browser behaviour, thedistbuild, and whether #8973 duplicates any card other than #8961. The reviewer read objectstack at the shared checkout's pinned hasheabdd66frather than at objectstackorigin/main.
Generated by Claude Code
Release:branchclaude/issue-8767-object-grid-refuses-string-sort· PR #8960 MERGED asc5faf0635387a89f1503929d617fc8c6e5661fb9— verified on the tree, ⛔ not from an API field.Landing established by
git merge-base --is-ancestor→ rc 0 in a fresh clone, with the reversed probe as a negative control → rc 1.⚠️ Field gap again: commit2026-09-10 15:01:47Zvsmerged_at 2026-09-10T15:19:31Z— 17m44s, a sixth sample in the same direction.ObjectGridnow refuses the retired stringsortclause at its own read site. The array arm, the join-string wire shape, the export path andparseSchemaSortare untouched.What it landed on
- Contract review, round 2 (delta):
5619296255—VERDICT: PASSatCONTRACT_REVIEW_TIER, judged head52d28eb4b4551fffe3ffc9a46fde2d532fc99785, superseding the round-1 REWORK at5618090408(which judged a head that then moved). Declared SELF-REVIEW — reviewer and implementer are both subagents of one dispatching session; ⛔ recorded as such, never treated as an independent clearance. - Both round-1 items landed: the two false contract sentences are gone from the changeset, and
serverSorting.test.tsx's frozen-defect leg was re-authored with the arrow/wire divergence now pinned explicitly (both halves asserted, not just the arrow) and the falsedescribetitle fixed. - Carriers stripped from both card and PR before landing, with the provenance comment citing the record id and the judged head.
⭐ The distinction that mattered
The changeset had claimed the array spelling is "the only one
@objectstack/specaccepts." False:ObjectGridPropsSchema.sortisz.unknown(), so the protocol's validator parses a string. What the protocol declares is the array of{ field, order }— its owndescribe, its sibling's typedz.array(SortItemSchema)atui/component.zod.ts:2231, and thedefaultSortretirement text at:2489-2498all say so. ⇒ a sentence about the validator and a sentence about the declaration are not interchangeable, and route C moves toward the protocol either way.Successors
- objectui#8961 — after this PR
parseSchemaSortstill parses the retired string spelling the fetch path now refuses, so a grid authoredsort: 'name desc'paints a descending arrow over an unordered list.pm:blockedon two conditions; this PR landing clears the first. - objectui#8973 — the untouched array arm emits
$orderby: 'name undefined'whenorderis omitted. ⛔ Deliberately out of scope here. Triaged by another seat todomain:ui. - Two nits carried, ⛔ neither in this diff:
packages/types/src/objectql.ts:695andsort-query.ts:42still read "orderis optional and means'asc'" above a declaration whereorderis required (SortItemSchema.ordercarries no.optional()). Both are objectui#8758's text; they belong on finding(plugin-grid):object-grid's private array-arm lowering interpolates missing keys into$orderby— aSortConfigwithorderomitted goes out asname undefined#8973.
Labels
pm:dispatchedand the assignee are left on this closed card deliberately —Fixescloses the card and never its labels, and the pair is the record of which seat drove it.
Generated by Claude Code
- Contract review, round 2 (delta):
- added a commit that references this issue
on Sep 16, 2026
Filed unassigned by the
domain:spec@objectuiPM seat (sessionsession_01Jmxdo7bmeqCQHLSfmLVX9w). ⛔ Not claiming. Grading is the triage seat's.os-devseat's, taken while implementing #8221 (PR #8758), and this seat has NOT independently re-derived it. It is filed rather than folded into #8758 because the dev's own recommendation was a follow-up card, not a rider — and this seat agrees, for the reason in the appetite section. ⛔ Re-derive before implementing.The defect
The #8221 ruling (decision batch #77, maintainer 「其他同意」,
5567944420) retires the legacy stringsortclause: one spelling, the array, everywhere. Its item 3 rejects option A by name, and the reason it gives is precisely the shape this card is about:PR #8758 narrows the shared sink
convertSortToQueryParamsand every declaration that published a string arm. Butpackages/plugin-grid/src/ObjectGrid.tsx:1852-1858does not use that sink. It lowersschema.sortwith its own private code and forwards a runtime string straight to$orderby(for the array arm it emits a comma-joined'field order'string).⇒ After #8758 lands, a bare
object-gridstill honours the retired spelling at runtime, while the same key routed throughobject-viewis refused with a diagnostic. One key, two meanings, chosen by which block you are on — the exact thing the ruling declined to ship.The three routes, and why the cheap-looking one is wrong
type-mismatch, so the grid's string handling becomes dead tolerance for a later reader to remove. ⛔ But this is a silent per-block difference in exactly the shape option A was rejected for; "dead tolerance" that still changes$orderbyis not dead.ObjectGrid.schema.sortthroughconvertSortToQueryParams. Completes the ruling's "everywhere".ObjectGridtoday sends a comma-joined string, and the same value is read by the export path (:3022) and the header-arrow resolverparseSchemaSort(:4006). Blast radius well beyond one card, and it needs its own measurement of the server contract and both readers.ObjectGridREFUSE a string arm with the same diagnostic before its own lowering. ⭐ Small, does not touch the array path, closes the divergence, and reuses the diagnostic feat(core)!: retire the legacy stringsortclause — one spelling, the array (objectui#8221) #8758 already ships.The implementing seat's recommendation, and this seat's, is C — the only option that closes the divergence the ruling cared about without changing
$orderbyfor every existing grid. ⛔ But the choice is not an implementing seat's to make: B and C differ in blast radius and C leaves the wire shape divergent even after the accept sets agree. Routed to the decision box.Appetite, if C is ruled
In scope: the refusal + diagnostic at
ObjectGrid's own read site, reusing #8758's reporter (deduped once per spelling), and a pin that a bareobject-gridrefuses the clause while the array arm still lowers unchanged.⛔ Falls off the back: the wire shape (that is B, and it is a different card); the export path and
parseSchemaSort; anything aboutobject-calendar, whose local face declaressort?: anyand is wider than the retired union anyway (plugin-calendar/src/ObjectCalendar.tsx:76— observation only, no defect, #8171 and the ruling's item 4 own that block).Grading note
Clause-② depends on the route: C is a narrowing of what a grid accepts at runtime (a string that lowers today stops lowering) ⇒ same shape as #8221, non-additive on a published face. B additionally moves the wire shape. Neither is a widening.
Refs: #8221 (the ruling) · PR #8758 (the retirement, in flight) · #8171