Skip to content

feat(core)!: retire the legacy string sort clause — one spelling, the array (objectui#8221) - #8758

Merged
os-bill merged 2 commits into
mainfrom
claude/issue-8221-retire-legacy-string-sort
Sep 9, 2026
Merged

os-bill merged 2 commits into
mainfrom
claude/issue-8221-retire-legacy-string-sort

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes #8221

Director ruling, decision batch #77 (2026-09-07), option B, recorded at issue comment 5567944420: the legacy string sort clause is retired. One spelling, the array. Option A (per-block string arms) was rejected by name.

⛔ Draft on purpose, and it stays draft. Clause ② is yes on this card (an accept set moves on published @object-ui/core), so under the enqueue bar written today this PR is built at the default tier and gated by a ceiling-tier contract review before it may be enqueued. This seat has not flipped ready, has not enqueued, has not armed auto-merge, and has not touched the needs:contract-review label — that carrier is already on the card.


1. What changed

surface change
packages/core/src/utils/sort-query.ts convertSortToQueryParams drops the string arm; signature narrows to QuerySortEntry[]; docblock rewritten; a string that still arrives at runtime is refused with a diagnostic naming the array form
packages/types/src/objectql.ts ObjectGridSchema.sort, ObjectMapSchema.sort, ObjectGanttSchema.sort narrow to SortConfig[]
packages/types/src/zod/objectql.zod.ts the same three mirrors narrow from a z.union to z.array(SortConfigSchema) — TS face and mirror move together
packages/app-shell/src/utils/deriveRelatedLists.ts the ListView reader's list.sort narrows
packages/plugin-form/src/LineItemsPanel.tsx, packages/plugin-timeline/src/ObjectTimeline.tsx the local sort inputs narrow
docs content/docs/plugins/plugin-map.mdx, content/docs/plugins/plugin-view.mdx, packages/plugin-view/README.md teach the array only
example examples/schema-catalog/src/schemas/plugin-view/object-view-list.json authored the retired clause; migrated
tooling scripts/check-doc-example-types.mjs ledger row re-keyed (its key is FILE:LINE and objectql.ts grew three lines)

2. The surface, re-derived on this head — and it differs from the dispatched list

git grep -nE "sort\??:\s*string\s*\|" on 1492fc30, plus the zod unions (which that pattern cannot see) and the seven convertSortToQueryParams call sites, enumerated rather than searched for by spelling:

packages/core/src/utils/sort-query.ts:61                    the helper
packages/types/src/objectql.ts:687 · :2339 · :2724          (dispatched list said :687 · :2307 · :2692)
packages/types/src/zod/objectql.zod.ts:226 · :858 · :1053   (dispatched list said :194 · :826 · :1021)
packages/types/src/record-components.ts:172                 (dispatched list said :126)  ⛔ NOT NARROWED — see §6
packages/app-shell/src/utils/deriveRelatedLists.ts:143
packages/plugin-detail/src/synth/buildDefaultPageSchema.ts:211                            ⛔ NOT NARROWED — see §6
packages/plugin-form/src/LineItemsPanel.tsx:71
packages/plugin-timeline/src/ObjectTimeline.tsx:122
content/docs/plugins/plugin-map.mdx:112                      (dispatched list said :110)
packages/plugin-view/src/__tests__/ObjectView.filterSources.test.tsx:170   comment only, not in the dispatched list

Population differs in two directions. One hit the dispatched list did not carry (a stale comment in a plugin-view test), and — the load-bearing one — two of its hits are a different string dialect entirely and are deliberately left alone. §6.

Call sites of the helper — seven, as the card said, all still compiling after the narrowing:

packages/plugin-calendar/src/ObjectCalendar.tsx:479     schema.sort  (locally typed `any`; unaffected)
packages/plugin-form/src/LineItemsPanel.tsx:155         schema.sort  (narrowed here)
packages/plugin-gantt/src/ObjectGantt.tsx:739           schema.sort  (ObjectGanttSchema, narrowed)
packages/plugin-map/src/ObjectMap.tsx:796               schema.sort  (ObjectMapSchema, narrowed)
packages/plugin-timeline/src/ObjectTimeline.tsx:311     schema.sort  (narrowed here)
packages/plugin-view/src/ObjectView.tsx:973             the resolved view/table sort
packages/app-shell/src/utils/deriveRelatedLists.ts:161  cast, so the narrowing is on the declaration at :143

Workspace type-check is green across all 81 tasks, which is the check that every call site still compiles.

3. The accept set, MEASURED — against the built dist, every refusal paired with a working control

Probe imported packages/core/dist/utils/sort-query.js (the published face, not src) and counted console.error calls:

case input lowered to diagnostics
REFUSED legacy clause "name desc" undefined 1
REFUSED legacy asc "name asc" undefined 1
REFUSED bare field "name" undefined 1
REFUSED mixed case "name DESC" undefined 1
REFUSED whitespace-only " " undefined 1
CONTROL array desc [{"field":"name","order":"desc"}] {"name":"desc"} 0
CONTROL array, order omitted [{"field":"name"}] {"name":"asc"} 0
CONTROL multi-key, order kept [{stage,asc},{amount,desc}] {"stage":"asc","amount":"desc"} 0
CONTROL empty string "" undefined 0
CONTROL undefined undefined undefined 0
CONTROL number 42 undefined 0
CONTROL bare object {"name":"desc"} undefined 0

What a caller can no longer pass: the first four rows. Before this PR each of them lowered — the assertions deleted from packages/core/src/utils/__tests__/sort-query.test.ts at f6205c10 state exactly that ('name desc' → {name:'desc'}, 'name asc' → {name:'asc'}, 'name' → {name:'asc'}, 'name DESC' → {name:'desc'}), and those assertions were green on main.

The controls are what make the four refusals a verdict: a sink that refused everything would satisfy them and fail the eight rows below.

⚠️ One measured boundary worth stating because it is asymmetric: "" is falsy and short-circuits on the !sort guard before any spelling is inspected, so it stays silent (it means "nothing was authored"). " " is truthy, so it is a string and is reported. Both were undefined before this PR too; only the diagnostic is new.

4. The runtime string — the case the types cannot stop, and its pin

Types are erased. A JSON document, a stored metadata row, or an as any bag still reaches the sink carrying "name desc", and that path is live in this repo today: deriveRelatedLists reads object.list.sort from platform view metadata through a cast.

What happens now, pinned end to end rather than asserted:

  • the sink returns undefined, so the query carries no $orderby;
  • console.error fires once per spelling (module-state dedupe, the house form reportRetiredFieldType already uses, with an exported resetRetiredSortSpellingReports() test seam);
  • the message names the array form, quotes what arrived, and states the consequence:
[object-ui] convertSortToQueryParams: the legacy string `sort` clause is retired (objectui#8221)
and was REFUSED — received "name desc", so this query carries no `$orderby`. Write the array form
instead: sort: [{ field: 'name', order: 'desc' }] (`order` is optional and means `'asc'`). The
array is the only spelling every `sort` input declares, and the only one `@objectstack/spec` accepts.

console.error and not warn, and not dev-gated: this refuses an authored row order, so the page renders in a different order than the author asked for — the same severity class as reportRetiredFieldType, which is also unconditional.

Pins, five files:

  • packages/core/src/utils/__tests__/sort-query.test.ts — refusal, message content, once-per-spelling with a different-spelling control, and silence for values that were never the clause (with a firing control on the same spy);
  • packages/types/src/__tests__/sort-string-arm-retired-8221.test.ts (new) — the three zod mirrors refuse the clause and still accept the array; type-level equality on both the interface face and the mirror's z.input face;
  • packages/app-shell/src/utils/__tests__/deriveRelatedLists.inheritSort.test.ts — the inherit boundary refuses and reports; the seven characters seq_no desc still never travel as a field name;
  • packages/app-shell/src/views/RecordDetailView.relatedListInheritedSort-5795.test.tsx — the same, end to end on the wire, with the parent scope and $top as live controls that the query really ran;
  • packages/plugin-view/src/__tests__/ObjectView.sortSink.test.tsx — table.sort refused at the real read site, with the array arm on the same read site as the control.

5. Ablation — the pin reddens, and the restore is proven by hash

Mutation: delete the reporter call, keep the refusal (i.e. restore the silent drop). Predicted direction before running: reddens, and specifically only on the diagnostic assertions.

pre-mutation  reporter-call lines: 1
post-mutation reporter-call lines (must be 0): 0
post-mutation ABLATED marker lines (must be 1): 1
working-tree blob 20de00a1… != HEAD blob f01f6d1a…        (the mutation really reached disk)

Test Files  4 failed (4)
Tests       8 failed | 24 passed (32)
every failure: AssertionError: expected "error" to be called 1 times, but got 0 times

RESTORE OK: blob f01f6d1ac503ff8257a3822363b81c6721a5ec1f == HEAD f01f6d1ac503ff8257a3822363b81c6721a5ec1f

Run from a script carrying trap restore EXIT INT TERM with an absolute REPO_ROOT; restore is git checkout HEAD -- path (never the bare form, which takes the polluted index) and is verified by blob hash against HEAD, an empty git diff HEAD for the path, and grep counts back to 1 and 0. Re-run after restore: 6 files, 86 tests, all green.

The 24 that stayed green under ablation include every toBeUndefined() refusal assertion — which is the point: the pins are about the diagnostic, not merely about the refusal, and a silent drop cannot satisfy them.

⚠️ No dist preflight applies to this ablation and none was faked: the root vitest config aliases @object-ui/core to packages/core/src (vitest.config.mts:498), so every one of those pins resolves to source. The dist measurement is §3, which imports the built file directly.

6. Deliberately NOT touched — measured, not assumed

Two hits on the dispatched surface list belong to a different string dialect. packages/types/src/record-components.ts:172 and packages/plugin-detail/src/synth/buildDefaultPageSchema.ts:211 are record:related_list.sort, whose string arm is the OData-ish 'field' / '-field' form. It is read by RelatedList.normalizeSortSpec (packages/plugin-detail/src/RelatedList.tsx:261-275), a normalizer of its own, and it never reaches convertSortToQueryParams. Retiring it is not what was ruled, and would delete working behaviour with no ruling behind it. Both were narrowed, measured, and reverted byte-identically (blob hashes verified against HEAD) once the dialect was established.

The spec still accepts the clause where the platform, not this repo, is the producer. Measured on @objectstack/spec@17.3.0 with controls on the same call:

schema 'name desc' [{field,order}] 42 control
ListViewSchema PARSES PARSES REFUSED sort/invalid_union bogusProp refused by name
RecordRelatedListProps PARSES PARSES REFUSED sort/invalid_union bogusProp refused by name
ElementDataSourceSchema REFUSED invalid_type — REFUSED bogusProp refused by name
ObjectGridPropsSchema / ObjectCalendarPropsSchema PARSES PARSES PARSES bogusProp refused by name

⇒ a platform view record carrying sort: 'name desc' is still spec-legal today and stops being inherited by a derived related list after this PR — loudly, which is the whole reason the diagnostic exists. The spec-side pull-back is its own card; the ruling's item 4 already routes that class to objectstack. plugin-list's ListView.parseSortConfig reads that same spec-blessed slot and is untouched.

⚠️ The one residual asymmetry a reviewer should weigh. packages/plugin-grid/src/ObjectGrid.tsx:1852-1858 lowers schema.sort itself — it does not use the shared sink, and it forwards a runtime string to $orderby verbatim (for the array arm it emits a comma-joined "field order" string). So after this PR a bare object-grid still honours the retired spelling at runtime while the same key through object-view is refused. That state is not introduced here (the two paths already differed), but it is the "one key meaning different things on different blocks" the ruling rejected, and closing it changes the $orderby wire shape for every grid — a behaviour change well beyond this card. Reported to the PM rather than taken.

packages/plugin-calendar/src/ObjectCalendar.tsx:76 declares sort?: any, which is wider than the retired union, so the narrowing does not reach calendar's local face. Noted, not filed.

STRING_ARM_REGISTERED_TYPES exported by @objectstack/spec is a false friend — it is the ledger for the component TYPE union's open string arm (record:line_items), nothing to do with sort. objectui references it nowhere.

7. Changeset, and how it was graded

.changeset/8221-retire-legacy-string-sort.md declares seven released packages. @object-ui/core is minor, not major — the rule applied is AGENTS.md 版本号策略 line 240, verbatim: objectui's own breaking changes are scored minor with the breaking semantics written in the body, because every package sits in one fixed group and a single major would carry all of them off the @objectstack major this repo is pinned to. scripts/check-changeset-no-major.mjs enforces it. The breaking semantics are in the changeset body.

8. Gates — command, exit code, and what it printed

gate exit note
turbo run build --filter=!@object-ui/site --concurrency=2 0 43 successful, 43 total
turbo run type-check --concurrency=2 --continue 0 81 tasks; each touched package's own type-check includes its tsconfig.test.json leg
pnpm exec vitest run (repo root, path filters) 0 see §9 for the batches
turbo run lint on the seven touched packages 0 warnings only, all on untouched root files
node scripts/check-changeset-presence.mjs 0 19 published source files of 7 packages, 1 changeset declared
node scripts/check-changeset-no-major.mjs 0 no major declared
node scripts/check-changeset-fixed.mjs 0
pnpm check:control-bytes 0 6941 tracked text files scanned
pnpm check 0 ⚠️ it moved: the "did not validate" warning list went 4 → 3 when the schema-catalog fixture was migrated, which is the firing control that the count is real
pnpm check:doc-types / :doc-fences / :doc-snippets / :readme-exports / docs:check-links 0 each 635 of 635 doc snippets compile
the 15 check:* gates ci.yml runs per PR, plus type-check:scripts 0 each phantom-deps, self-import, unreferenced-sources, doc-example-readers, handler-key-reads, published-tsconfig-exclude, side-effects-array, element-data-source-declaration, esm-specifiers, spec-symbols, action-forward-parity, designer-field-key-parity, icon-record-names, i18n-keys, i18n-drift
node scripts/check-governed-queue-guard.mjs --test on all 25 paths 0 NOT GOVERNED — 25 paths against 5 governed surfaces, none matched
node scripts/check-doc-example-types.mjs 1 ⛔ pre-existing red on main, zero delta from this branch — see below

On that last one. Measured in a separate detached worktree at the merge base f6205c10, installed and built with the command the gate itself prescribes: the gate exits 1 there too, with exactly the same single failure (packages/types/src/zod/imported-defaults.ts:318 stripImportedDefaults, TS2304), which arrived with PR #8721 two hours before this branch. Filed as #8757. This branch adds no new failure to it, and it had to touch that file only to re-key a FILE:LINE ledger row its own diff moved. The gate is in package.json and in no workflow, so CI never runs it — measured here or nowhere.

9. Test batches, with their real numbers

paths files tests
packages/core/ packages/types/ 287 passed 5840 passed
packages/app-shell/ packages/plugin-form/ packages/plugin-timeline/ packages/plugin-map/ 795 (4 failed first, all four re-run green) 7687 (6 failed first)
packages/plugin-view/ packages/plugin-grid/ packages/plugin-list/ packages/plugin-detail/ packages/plugin-calendar/ packages/plugin-gantt/ 472 (1 failed first, re-run green) 4336 (2 failed first)
packages/plugin-view/ apps/console/ examples/ scripts/ 293 (1 failed first, re-run green) 7315 (1 failed first)

Every first-round failure was a fixture or pin that named the retired arm, and every one was triaged rather than batch-rewritten: three files had the spelling changed because the string was only a vehicle; four had pins replaced because what they pinned was the arm that is gone. Vitest was run from the repository root throughout, so objectui#3378's guard is satisfied — each run printed RUN v4.1.10 /home/user/objectui-issue-8221.

10. NOT MEASURED

  • Build Docs, check:node-esm-load, check:published-dist — not per-PR gates, and not run here.
  • pnpm test in full (all 4 CI shards) — the packages this diff can reach were run above; the remainder (components, react, fields, layout, i18n, data-*, mobile, providers, permissions, auth, collaboration) contain no reference to convertSortToQueryParams or to the narrowed declarations, and workspace type-check is green across all 81 tasks. CI runs the full sharded suite.
  • test:dist, test:e2e, test:e2e:live, performance-budget — not run here.
  • Browser dogfooding — not done. Nothing renders differently; what changes is a query parameter and a console line.
  • Whether the residual ObjectGrid string path (§6) is acceptable — not decided here; it needs a ruling, and it is reported rather than taken.
  • Whether check:doc-examples should join a workflow — named in decision(ci): check:doc-examples is declared in package.json and run by no workflow — wire it, or record that it is hand-run only #8757, not decided here.

Session for this implementation: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w

🤖 Generated with Claude Code

https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w


Generated by Claude Code

…he array

`convertSortToQueryParams` drops the string arm (objectui#8221, director
ruling, decision batch #77, option B). Its signature narrows to
`QuerySortEntry[]`, and a string that still reaches it at runtime is REFUSED
with a `console.error` naming the array form, quoting what arrived and stating
that the query carries no `$orderby` — never a silent drop.

Every declaration that published a string arm narrows with it, TypeScript face
and zod mirror together: `ObjectGridSchema.sort`, `ObjectMapSchema.sort`,
`ObjectGanttSchema.sort`, plus the local `sort` inputs on `LineItemsPanel`,
`ObjectTimeline` and `deriveRelatedLists`'s ListView reader. Docs teach the
array only.

Deliberately untouched, measured rather than assumed: `record:related_list`'s
`'field'` / `'-field'` string is a DIFFERENT dialect, normalized by
`RelatedList.normalizeSortSpec`, and never reaches this sink.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
… and re-key the moved doc-example ledger row

`examples/schema-catalog/src/schemas/plugin-view/object-view-list.json` authored
`table.sort: "name asc"` — the retired clause. `pnpm check` reported it by name
("did not validate as an ObjectUI schema"), and `objectui validate` gave the
reason: `table -> sort` expected array, received string. Migrated to
`[{ "field": "name", "order": "asc" }]`; the warning count moved 4 -> 3, which
is the firing control that the fixed file is the one that moved.

`scripts/check-doc-example-types.mjs`'s ledger is keyed by FILE:LINE, and the
`objectql.ts` edits shifted `ObjectFormSchema`'s example block 1604 -> 1607.
Re-keyed; `scripts/__tests__/check-doc-example-types.test.ts` is green again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3483.6 KB 3512.7 KB
Main entry chunk (gzip) 144.0 KB 350 KB
Entry file index-DOptX57t.js —
Status PASS —

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

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 15.67KB 5.75KB
app-shell (runtime-config.js) 20.68KB 7.36KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 499.42KB 114.32KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 10.12KB 3.28KB
data-objectstack (index.js) 198.39KB 55.29KB
fields (index.js) 244.96KB 61.76KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 6.57KB 2.76KB
i18n (index.js) 3.65KB 1.47KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 34.34KB 9.17KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.84KB 10.94KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 4.39KB 1.66KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 13.52KB 4.88KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.39KB 3.10KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.16KB 3.68KB
plugin-calendar (index.js) 49.00KB 13.91KB
plugin-charts (index.js) 71.39KB 19.92KB
plugin-chatbot (index.js) 194.53KB 46.34KB
plugin-dashboard (index.js) 131.43KB 34.44KB
plugin-designer (index.js) 215.51KB 44.29KB
plugin-detail (index.js) 252.39KB 65.32KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 134.20KB 33.48KB
plugin-gantt (index.js) 167.16KB 40.99KB
plugin-grid (index.js) 210.67KB 57.19KB
plugin-kanban (index.js) 55.50KB 15.75KB
plugin-list (index.js) 112.73KB 27.69KB
plugin-map (index.js) 20.49KB 6.83KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.42KB 11.92KB
plugin-timeline (index.js) 30.10KB 8.74KB
plugin-tree (index.js) 9.55KB 3.32KB
plugin-view (index.js) 84.54KB 20.84KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 81.07KB 26.86KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 6.58KB 2.74KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 5.55KB 2.45KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 13.64KB 4.59KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 1.00KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.93KB 1.49KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.74KB 2.25KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 4.73KB 2.28KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 14.27KB 5.47KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

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

Copy link
Copy Markdown
Collaborator Author

Final consolidated readings, taken after the last commit (1492fc30)

§9 of the description lists the test batches in the order they were run, including the first-round failures that the triage then fixed. Two whole-tree readings were taken afterwards, on the final head, so that table is not the last word:

Tests — one run over every package this diff can reach, from the repository root:

pnpm exec vitest run packages/core/ packages/types/ packages/app-shell/ packages/plugin-form/ \
  packages/plugin-timeline/ packages/plugin-map/ packages/plugin-view/ packages/plugin-detail/ \
  packages/plugin-grid/ packages/plugin-list/ scripts/ examples/

RUN v4.1.10 /home/user/objectui-issue-8221
Test Files  1622 passed | 2 skipped (1624)
Tests       23048 passed | 4 skipped (23052)
Duration    2233.91s
exit 0

Types — the whole workspace, nothing filtered out:

pnpm exec turbo run type-check --concurrency=2 --continue
Tasks:  81 successful, 81 total
Time:   9m35.681s
exit 0

Zero failures in either. Every first-round failure named in §9 is covered by this run, so "re-run green" there is now a whole-tree reading rather than a targeted one.

⚠️ Unchanged by the above and still true: node scripts/check-doc-example-types.mjs exits 1, and it exits 1 identically on an unmodified merge base — that is #8757, not this branch. And this pull request stays a draft until a ceiling-tier contract review passes; nothing here should be read as a request to enqueue it.

Session for this implementation: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w


Generated by Claude Code

os-bill commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Ceiling contract review: PASS — clearing the carrier and enqueueing. ⛔ But §6 of the body carries one sentence that is FALSE, and it is corrected here rather than merged unchallenged

domain:spec @ objectui seat, session session_012W3vMLTFY9SPr2LyxhSeYi, reading taken 2026-09-09T05:1xZ (clock re-read immediately before this stamp). Full verdict, with all six areas and the reviewer's declared limits, is on the card: objectui#8221 comment 5595976918.

⛔ The false sentence

§6 states:

after this PR a bare object-grid still honours the retired spelling at runtime while the same key through object-view is refused.

The second half does not hold. Measured by the ceiling reviewer through the real ObjectGrid, not argued:

path $orderby retirement diagnostics
object-view, default view type "name desc" 0
object-view, explicit grid "name desc" 0
object-view, calendar none 1
object-view, kanban none 1

ObjectView.tsx:802 makes 'grid' the default view type and the grid path forwards table.sort verbatim. ⇒ the refusal holds only for non-grid view types; through the default path the retired spelling is still honoured, silently.

⚠️ ⛔ The body is not edited. This correction sits beside it — 「事后证伪同处公开作废」 — and it is posted on the PR as well as the card because this body becomes the squash commit message, and a reader of main's history would otherwise inherit the claim. ⇒ Whoever reads §6 later: the honest sentence is "…while the same key through object-view in its non-grid view types is refused."

⭐ It changes the scope of the follow-up, ⛔ not the verdict. Nothing this PR ships is wrong; the escalation was correct and the reviewer's judgement is that the asymmetry is a separate card rather than incoherence here — the ruling names the helper, its signature and the docs, ⛔ not ObjectGrid's private lowering, and closing it rewrites ≥4 pinned wire-shape tests.

Landing bar, each item measured

gate reading
ceiling review PASS, tier-verified before adoption: 134 transcript model stamps, all claude-fable-5-1, zero residue (positive control: 182 "role" tokens in the same file)
check-clause2-carriers --pair 8758 exit 0 — both carriers readable in the fixed spelling and agreeing
CI on 1492fc30 33 check runs, all completed, ZERO failures, none in progress. 3 skipped (Test coverage ×2, dependabot). ⭐ Build Docs ran 3m55s here — a real build, not the 13-second #8647 false-green, because this diff touches content/
path face not governed — taken from the changed-file list, and independently confirmed by the green Governed Surface Queue Guard
follow-ups owed before clear filed: objectui#8764 (the grid delegation path, with the corrected scope — object-grid and object-view in grid mode and parseSchemaSort) and objectstack#17053 (ListViewSchema.sort's string arm, which objectstack#16553 does not cover)

⭐ Two findings the review added that the body did not claim, recorded so they are not lost: the retirement also refuses "-name" and "name, amount desc", which the old helper lowered to the garbage fields {"-name":"asc"} / {"name,":"asc"}; and a fourth face moved by derivation — ObjectViewSchema.table.sort — which §1/§3 should have listed.

⇒ Carrier cleared on both card and PR, draft flipped, enqueued. ⛔ Not hand-merged; ⛔ this seat never merges its own lane's PRs — the queue lands it.


Generated by Claude Code

@os-bill
os-bill marked this pull request as ready for review September 9, 2026 04:51
@os-bill
os-bill added this pull request to the merge queue Sep 9, 2026
Merged via the queue into main with commit 9a853f2 Sep 9, 2026
35 checks passed
@os-bill
os-bill deleted the claude/issue-8221-retire-legacy-string-sort branch September 9, 2026 05:21
os-warren pushed a commit that referenced this pull request Sep 10, 2026
…rid's own read site (objectui#8767)

`ObjectGrid` reads `schema.sort` and lowers it with PRIVATE code, so PR #8758's
narrowing of the shared sink `convertSortToQueryParams` never reached it: after
that PR a bare `object-grid` still forwarded a runtime string verbatim to
`$orderby`, while the SAME key routed through `object-view` was refused with a
diagnostic. One key, two meanings, chosen by which block you are on — the
per-block divergence the objectui#8221 ruling declined by name.

Route C, as ruled by the maintainer 2026-09-10: keep the wire shape, refuse the
string. The string arm now calls the shared sink for its refusal alone — that
reporter names the offending spelling once per spelling and answers `undefined`,
so the query carries no `$orderby` — and the return value is deliberately
unused, because the array arm keeps lowering to this block's own
`"field order"` join string. Route B (routing the whole key through the sink,
which would send its `{field: direction}` map instead) was explicitly not taken;
the export path and the header-arrow reader `parseSchemaSort` are untouched.

- `gridRetiredStringSort-8767.test.tsx` pins BOTH halves, so the pin cannot pass
  by refusing everything: a string reaches no `$orderby` and reports once per
  spelling; the array arm still emits `'name desc'` and `'status asc, name desc'`
  with no diagnostic.
- `serverSorting.test.tsx`'s "replaces the declared sort" case authored the
  retired string and asserted it went out verbatim — that assertion is this
  card's own subject, so its fixture is re-authored in the declared array
  spelling and its #3106 subject is preserved.
- Two comments this change falsifies are corrected, comment-only: the
  `parseSchemaSort` docblock no longer claims the fetch path reads all three
  spellings, and both it and the header-arrow read site now record that this
  reader is WIDER than the fetch path and that closing the gap is route B.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w
hotlong pushed a commit that referenced this pull request Sep 16, 2026
… (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
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 17, 2026
…rid's own read site (objectui#8767) (objectstack-ai#8960)

* fix(plugin-grid): refuse the retired string `sort` clause at object-grid's own read site (objectui#8767)

`ObjectGrid` reads `schema.sort` and lowers it with PRIVATE code, so PR objectstack-ai#8758's
narrowing of the shared sink `convertSortToQueryParams` never reached it: after
that PR a bare `object-grid` still forwarded a runtime string verbatim to
`$orderby`, while the SAME key routed through `object-view` was refused with a
diagnostic. One key, two meanings, chosen by which block you are on — the
per-block divergence the objectui#8221 ruling declined by name.

Route C, as ruled by the maintainer 2026-09-10: keep the wire shape, refuse the
string. The string arm now calls the shared sink for its refusal alone — that
reporter names the offending spelling once per spelling and answers `undefined`,
so the query carries no `$orderby` — and the return value is deliberately
unused, because the array arm keeps lowering to this block's own
`"field order"` join string. Route B (routing the whole key through the sink,
which would send its `{field: direction}` map instead) was explicitly not taken;
the export path and the header-arrow reader `parseSchemaSort` are untouched.

- `gridRetiredStringSort-8767.test.tsx` pins BOTH halves, so the pin cannot pass
  by refusing everything: a string reaches no `$orderby` and reports once per
  spelling; the array arm still emits `'name desc'` and `'status asc, name desc'`
  with no diagnostic.
- `serverSorting.test.tsx`'s "replaces the declared sort" case authored the
  retired string and asserted it went out verbatim — that assertion is this
  card's own subject, so its fixture is re-authored in the declared array
  spelling and its objectstack-ai#3106 subject is preserved.
- Two comments this change falsifies are corrected, comment-only: the
  `parseSchemaSort` docblock no longer claims the fetch path reads all three
  spellings, and both it and the header-arrow read site now record that this
  reader is WIDER than the fetch path and that closing the gap is route B.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w

* fix(plugin-grid): repair two false contract claims in the changeset, and pin the arrow-vs-wire divergence instead of freezing it green (objectui#8767)

Contract review VERDICT: REWORK, two bounded items. The refusal mechanism, the
control pins, the `minor` grading and objectui#8961's parking are judged correct
and are untouched — `ObjectGrid.tsx` is byte-identical to the reviewed head.

R1 — the changeset body carried two sentences that are false about the contract:

- "the only one `@objectstack/spec` accepts". Measured against the installed
  `@objectstack/spec@17.4.0`: `ui.ObjectGridPropsSchema.sort` is
  `z.unknown().optional()`, so `safeParse` PARSES `'name desc'`, `'name'`, `42`,
  `['name desc']` and `{ name: 'desc' }` alike, while an undeclared `bogusProp`
  is REFUSED with `unrecognized_keys` — the control that shows those readings
  are real. The protocol's validator does not refuse the string. What the
  protocol DECLARES is the array, in three places: the key's own `describe`
  ("Initial sort (array of { field, order })"), the sibling
  `ElementRecordPickerPropsSchema.sort` typed as `z.array(SortItemSchema)`
  (which refuses a string outright), and the `defaultSort` retirement text
  ("wrap the value in an array"). The sentence is now a claim about what the
  protocol declares, which is true, rather than about what it accepts.
- "(`order` is optional and means `'asc'`)". `SortConfig.order` is REQUIRED in
  `@object-ui/types` and in its zod mirror, and the protocol's `SortItemSchema`
  requires it too (measured: it refuses `[{ field: 'name' }]` with
  `invalid_value` at `0.order`). Worse, this block's untouched array arm
  interpolates whatever is there, so an omitted `order` lowers to
  `$orderby: 'name undefined'`. The changeset now says both keys are required
  and names that behaviour; the defect itself is pre-existing, is NOT widened
  into this diff, and is filed as a successor card.

R2 — `serverSorting.test.tsx` had a defect frozen green at the arrow case:

- its fixture is re-authored in the declared array spelling, the same principle
  this PR already applied to the sibling case;
- the string's behaviour is now an EXPLICIT pin naming objectui#8961, asserting
  both halves — the arrow is drawn AND the query carries no `$orderby` AND the
  disagreement is announced once. A test that asserted only the arrow was a
  green certificate that the state is intended, not a record of a divergence;
- the `parseSchemaSort` describe title ("the header reads what the fetch path
  reads") was falsified by this PR and is renamed to say what that block
  actually pins: the reader's own contract, wider than the fetch path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/objectui that referenced this pull request Sep 17, 2026
… (objectui#8961) (objectstack-ai#9604)

Fixes objectstack-ai#8961

Executes the maintainer ruling of 2026-09-15 — director batch objectstack-ai#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 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. objectui#8767 (route C) made this block's fetch path
REFUSE a string `sort` and 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

`parseSchemaSort` admits only `[{ field, order }, ...]` — the spelling
`@objectstack/spec` declares for `ObjectGridPropsSchema.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 objectstack-ai#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 `convertSortToQueryParams` and 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 `$orderby` and 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.
- Non-vacuity control in that case: the neutral sort indicator is still
rendered, so the two "no arrow" nulls cannot pass by the column having
quietly stopped offering sorting.
- The unit block re-judges every spelling it used to admit (bare string,
array-of-strings, mixed array), each refusal paired with a
declared-array control, single- and multi-key.
- `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 full
`pnpm 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.sort` itself (`Array.isArray(schemaSort)`), never
through `parseSchemaSort`, so it was already array-only and does not
move. The new docblock says so.

## Acceptance notes

- `plugin-view`'s `ObjectView.tsx` carries 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.
- The server-side export path maps an array of retired strings to an
empty `sort` array 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](https://claude.ai/code/session_01VCpmqvacV4BypY48QdoxcE)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

3 participants