Skip to content

refactor(plugin-view)!: retire the direction sort spelling from the published toSortItems export - #6234

Merged
yinlianghui merged 2 commits into
mainfrom
claude/issue-6011-tosortitems-direction
Aug 25, 2026
Merged

refactor(plugin-view)!: retire the direction sort spelling from the published toSortItems export#6234
yinlianghui merged 2 commits into
mainfrom
claude/issue-6011-tosortitems-direction

Conversation

@yinlianghui

Copy link
Copy Markdown
Collaborator

Fixes#6011

Retires the direction sort spelling from toSortItems, a published export of
@object-ui/plugin-view. The ruling is inherited, not made here: #4869 ruled that a
spelling the sink does not recognise gets ruled into the contract or rejected at the
producer, never absorbed by a tolerance layer; #5293 executed the identical retirement for
ObjectViewProps.views[].sort and shipped it minor. This is the same treatment on the
last dormant tolerance layer of the sort family's public surface.

Clause ② — contract review (published-export acceptance change)

What the export accepted before.toSortItems(draftSort) folded
s.order || s.direction || 'asc' per entry. It therefore accepted two spellings for one
key
{ field, order } and { field, direction } — and, when both were present,
silently preferred order. No warning, no rejection, no record that a second spelling was
in play.

What it accepts after.s.order || 'asc'. order only.direction is now an
unrecognised key like any other: read by nothing, reported by nothing.

Nothing else about the helper moved. id is preserved when present and minted with
crypto.randomUUID() otherwise; field still defaults to ''; a non-array draft still
yields []. The TypeScript signature is byte-identical ((draftSort: any): SortItem[]),
so no downstream consumer's types change — this is an acceptance change only, invisible to
the compiler on both sides.

Exact failure mode of an unmigrated caller. A caller passing
{ field: 'created_at', direction: 'desc' } used to get
{ id, field: 'created_at', order: 'desc' }. It now gets
{ id, field: 'created_at', order: 'asc' } — the documented default for an entry naming no
direction. The break is silent and it is a wrong answer, not an absent one: nothing
throws, nothing warns, the SortBuilder row renders, the field is right, and the list sorts
ascending while the caller asked for descending. That is the whole cost, and it is the
cost #5293's changeset already priced for the sibling surface.

Why the unknown out-of-tree caller is not a blocker. The card is right that this repo
cannot answer whether any host calls it (zero in-repo production callers). objectui's stated
policy — it ships its own breaks as minor with a changeset naming the retired key — is the
answer to that unknown, not a reason to keep the tolerant read.

The trap this card carries, and how the change was actually graded

toSortItems has no in-repo production caller. The one in-repo importer of the published
symbol, apps/console/src/__tests__/insecure-origin-crypto.test.ts, passes the canonical
order and only grades crypto.randomUUID. So no pre-existing suite could go red for this
change
— a green run of the existing suites proves nothing about the retirement.

The pin was therefore written first and measured red against the unfixed function
(packages/plugin-view/src/config/__tests__/view-config-utils.toSortItems.test.ts):

Tests 2 failed | 4 passed (6)
FAIL … > does NOT read the retired `direction` spelling — it falls back to `asc`
AssertionError: expected 'desc' to be 'asc'
FAIL … > ignores `direction` even when it is the only key that could answer
AssertionError: expected [ 'desc', 'desc' ] to deeply equal [ 'asc', 'desc' ]

Both flipped green with the one-line fix; the predicted failing pair and the predicted
received values matched the run exactly.

Which of these assertions would still pass on a revert

Stated plainly, because most of them would:

assertionsurvives a revert?
does NOT read the retired direction spelling — it falls back to ascNO — this is the retirement. Reverting returns 'desc'.
ignores direction even when it is the only key that could answerNO. Reverting returns ['desc','desc'].
reads orderyes — order was always read first
defaults a sort entry with no direction key at all to ascyes — the || 'asc' tail is unchanged
preserves an explicit id and mints one otherwiseyes — untouched behaviour
returns an empty list for a non-array draftyes — untouched behaviour

Two of six assertions grade the change. The other four are regression cover for the parts
deliberately not touched, and a revert leaves them green. Every other suite in the repo
also stays green on a revert — that is the point of the trap above, not an oversight.

Also in this diff

  • packages/plugin-view/src/ObjectView.tsx — the views prop doc paragraph recorded
    this export as folding s.order || s.direction and "deliberately not retired here
    (objectui#6011)". True when ObjectViewProps.views[].sort declares direction, but every sort consumer reads order — a host-supplied desc view silently sorts ascending #5293 landed, false the moment this does. Rewritten to say the
    retirement has since happened on its own card.
  • packages/plugin-view/src/__tests__/ObjectView.filterSources.test.tsx — the stale
    fixture the card recorded, which wrote direction behind an as any on table.defaultSort,
    a surface declaring order (packages/types/src/objectql.ts, zod mirror agreeing).
    Re-spelled after reading what it pins: the assertion pins verbatim pass-through of the
    forwarded value and never reads the direction key, so any word passed and re-spelling changes
    nothing it grades. A comment now records that the surviving as any is about arity
    (defaultSort is declared a single { field, order }, the fixture passes an array on
    purpose), not spelling — so the canonical key is not misread as declaration conformance.
  • packages/plugin-view/README.md:91 — the documented export line described the return
    shape but not the accepted one. Now names { field, order }.
  • packages/plugin-view/src/SortUI.tsx — deliberately untouched. Its file-local
    const toSortItems is a different symbol that maps SortEntry[], and direction is the key
    SortUISchema legitimately declares. A grep for toSortItems reads it as a caller; it is not
    one.
  • The .changeset entry is minor and names direction in words, so a host greping
    the retired key lands on it. It also records that ObjectViewProps.views[].sort declares direction, but every sort consumer reads order — a host-supplied desc view silently sorts ascending #5293's scope note — which said this export
    was not retired by that change — described that release's scope correctly and is superseded
    here.

Verification

All of the following on final head b0d186b79, with the working tree clean
(git diff HEAD --stat empty).

gateverdict line
vitest run packages/plugin-view apps/console/…/insecure-origin-crypto.test.tsTest Files 24 passed (24) / Tests 237 passed (237)
pnpm --filter @object-ui/plugin-view type-checkexit 0 — tsc --noEmit && tsc -p tsconfig.test.json
pnpm --filter @object-ui/console type-checkexit 0 (see note)
pnpm --filter @object-ui/plugin-view lint✖ 247 problems (0 errors, 247 warnings) — all pre-existing
check-changeset-presence.mjs✅ 4 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-no-major.mjs✅ No changeset declares a major bump.
check-changeset-fixed.mjs✅ All workspace packages are in the changeset fixed group.
check:control-bytes✅ check-control-bytes: OK (scanned 5140 tracked text file(s))
check:phantom-deps✅ Every in-scope import is declared by the package that publishes it.
check:self-import✅ No package names itself inside its own src/.
check:vi-mock-specifiers✅ check-vi-mock-specifiers: OK
check:shell-escape-residue✅ check-shell-escape-residue: OK
lint:coverage✅ lint coverage: 46/46 packages linted, 0 with outstanding errors (0 total).
type-check:coverage✅ type-check coverage: 45/46 via type-check … 0 errors outstanding

Note on the console type-check. It first failed with five Cannot find module errors for
@object-ui/app-shell / plugin-view / plugin-list / plugin-detail / auth — the
unbuilt-dependency shape, not this diff. After pnpm --filter '@object-ui/console^...' build
it is exit 0. Recorded because that red reads identically to "your change broke an import".

Declared narrowing.pnpm lint (turbo run lint, 46 packages) was not run whole; the
@object-ui/plugin-view lint task was run directly, which is the complete unit turbo would run
for this diff. Evidence the narrowing excludes nothing: (1) the population comes from eslint's
own config resolution, not a hand-picked file list; (2) --format json reports 38 files
linted, 0 errors, 247 warnings
; (3) eslint.config.js declares noparserOptions.project
and noprojectService, so type-aware linting is off and a change inside this package
cannot move the verdict on any file outside it — and each package's lint script is eslint .
rooted in its own directory. lint:coverage independently reports 46/46 packages at 0 errors.
CI runs the full farm regardless.


Generated by Claude Code

…spelling
`toSortItems` is a published export (re-exported from the package root and
listed in the README). It folded `s.order || s.direction || 'asc'` — two
spellings for one key, silently preferring the canonical one. That is the
tolerance layer objectui#4869 ruled against, and objectui#5293 retired the
same word on `ObjectViewProps.views[].sort`.
The fallback is gone: the export now reads `order` only. An unmigrated caller
passing `{ field, direction }` gets the documented `'asc'` default instead of
the direction it asked for — a silent behaviour break, shipped `minor` per
AGENTS.md 版本号策略 with a changeset naming the retired key so a host greping
`direction` lands on it.
The function had no in-repo production caller, so no existing suite could go
red for this. A dedicated pin now grades the retirement.
Also corrected: the `ObjectViewProps.views` doc paragraph that recorded this
export as deliberately not retired, and a fixture that wrote the retired
spelling behind an `as any` on a surface declaring `order`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
The README export line described the return shape but not the accepted one,
so a host greping the retired `direction` key found nothing here either way.
Naming `{ field, order }` puts the surviving spelling in the published doc.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3222.6 KB3990.2 KB
Main entry chunk (gzip)153.8 KB350 KB
Entry fileindex-iGQI602b.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)10.38KB3.90KB
app-shell (runtime-config.js)18.10KB6.51KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)505.28KB114.58KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)171.74KB47.48KB
fields (index.js)238.40KB59.89KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.66KB18.32KB
plugin-chatbot (index.js)188.21KB44.67KB
plugin-dashboard (index.js)133.35KB34.45KB
plugin-designer (index.js)212.32KB42.81KB
plugin-detail (index.js)244.13KB61.93KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)126.07KB30.78KB
plugin-gantt (index.js)164.15KB39.88KB
plugin-grid (index.js)201.14KB54.40KB
plugin-kanban (index.js)52.89KB14.59KB
plugin-list (index.js)111.94KB27.24KB
plugin-map (index.js)20.11KB6.64KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.49KB7.59KB
plugin-tree (index.js)9.26KB3.13KB
plugin-view (index.js)84.55KB20.74KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)54.84KB18.43KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.35KB0.70KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)12.13KB3.65KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)7.54KB2.63KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.49KB2.14KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@yinlianghui
yinlianghui marked this pull request as ready for review August 25, 2026 03:33
@yinlianghui
yinlianghui added this pull request to the merge queueAug 25, 2026
Merged via the queue into main with commit c5fbe0bAug 25, 2026
27 checks passed
@yinlianghui
yinlianghui deleted the claude/issue-6011-tosortitems-direction branch August 25, 2026 03:45
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationplugintests

Projects

None yet

2 participants

@yinlianghui@claude