Skip to content

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

Merged
os-warren merged 2 commits into
mainfrom
claude/issue-8767-object-grid-refuses-string-sort
Sep 10, 2026
Merged

os-warren merged 2 commits into
mainfrom
claude/issue-8767-object-grid-refuses-string-sort

Conversation

@os-warren

@os-warren os-warren commented Sep 10, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #8767

Authored by the os-dev seat of session https://claude.ai/code/session_01Jmxdo7bmeqCQHLSfmLVX9w. That reference is written here as prose in a code span on purpose: an attribution footer on a pull request body is normalised to the session-URL form on creation and degraded back to the bare form by every later edit, so a PR that is edited even once loses the reference from its footer. The code span survives the edit path.

Route C, as ruled by the maintainer on 2026-09-10: keep the wire shape; make ObjectGrid REFUSE a string schema.sort with PR #8758's own diagnostic, before its own lowering. Route B (routing the whole key through the shared sink) and route A (leave it) were both declined on the card and are not re-opened here.

Rework round — head 52d28eb4b. The contract review returned VERDICT: REWORK with two bounded items, adopted verbatim by the dispatching seat. The refusal mechanism, the control pins, the minor grading and #8961's parking were judged correct and are untouched: ObjectGrid.tsx is byte-identical to the reviewed head 76a9ead40 (git diff --name-only 76a9ead40..HEAD names only the changeset and one test file). What follows describes the PR as it now stands; the two repairs have their own section below.

Rework: the two repairs

R1 — two sentences in the changeset were false about the contract

Both were claims about the spec, and both were wrong. Re-derived on this branch rather than taken from the review.

(a) "the only one @objectstack/spec accepts". False. Measured against the installed @objectstack/spec@17.4.0 by calling ui.ObjectGridPropsSchema.safeParse directly:

input on sort verdict
'name desc' PARSES
'name' PARSES
42 PARSES
['name desc'] PARSES
{ name: 'desc' } PARSES
[{ field: 'name', order: 'desc' }] PARSES
an undeclared bogusProp — CONTROL REFUSED, unrecognized_keys

The control is what makes the six PARSES a reading rather than a schema that accepts anything. The cause is in the protocol source: ObjectGridPropsSchema.sort is z.unknown().optional(), so the protocol simply does not police this field.

z.unknown() is the protocol declining to police, not the protocol blessing the string — and what the protocol declares and documents is the array, in three independent places, all re-read on objectstack for this round:

  • the same key's own describe: Initial sort (array of { field, order });
  • the sibling ElementRecordPickerPropsSchema.sort, which spells the identical intent as a typed z.array(SortItemSchema) — measured: it PARSES [{ field: 'name', order: 'desc' }], REFUSES 'name desc' (invalid_type at the root) and REFUSES ['name desc'] (invalid_type at 0);
  • the defaultSort retirement text, which instructs authors to rename the key to sort and wrap the value in an array.

⇒ Route C moves this block toward the protocol's declared shape. But the sentence still had to be repaired rather than defended, because "the only one the spec accepts" is a claim about the validator, and the validator accepts the string. The changeset now makes the true claim — about what the protocol declares — and states plainly that the validator does not refuse the string.

(b) "(order is optional and means 'asc')". False twice over.

  • SortConfig.order is required on both declared faces: packages/types/src/objectql.ts (order: 'asc' | 'desc', no ?) and its zod mirror packages/types/src/zod/objectql.zod.ts (z.enum(['asc','desc']), no .optional()). The protocol agrees: its reusable SortItemSchema requires order — measured, z.array(SortItemSchema).safeParse([{ field: 'name' }]) refuses with invalid_value at 0.order.
  • And on this block the advice is actively harmful. The untouched array arm interpolates every key unconditionally, so an omitted order lowers to $orderby: 'name undefined'. That sentence came from the shared sink's own diagnostic, which this PR now makes object-grid print — so the grid was prescribing a migration that produces garbage on the grid.

The changeset now says both keys are required and names that behaviour. ⛔ The defect itself is pre-existing on the arm this PR does not touch, and widening the diff to reach it is the direction the ruling refused. It is therefore out of scope here, and carried instead by #8973, which holds the full probe table. (Deliberate wording: a closing keyword must never sit near another card's number — the merge parser matches keyword-plus-reference and does not read negation, so a sentence saying a card is not addressed would close it anyway.)

R2 — a defect was frozen green in serverSorting.test.tsx

The review is right, and right by this PR's own principle. The sibling case at :141 was re-authored in the array spelling precisely so a pin would stop asserting something the wire no longer does; the arrow case was then left authoring the retired string, green, while the wire carries no ordering — the same shape, kept. Leaving the divergence visible was the correct instinct; leaving it visible as a green test that asserts only the arrow is not, because a green test does not expose a state, it certifies it.

Three edits, one stroke:

  1. the arrow case's fixture is re-authored in the declared array spelling, for the same reason :141 was;
  2. the string's behaviour becomes an explicit pin naming finding(plugin-grid): after #8767 the header arrow still reads the retired string sort spelling the fetch path now refuses — parseSchemaSort is wider than the wire #8961, asserting both halves — the arrow is drawn, hasOwnProperty(params, '$orderby') is false, and exactly one retired-spelling diagnostic is emitted. That case is now the one that has to change when finding(plugin-grid): after #8767 the header arrow still reads the retired string sort spelling the fetch path now refuses — parseSchemaSort is wider than the wire #8961 closes the gap;
  3. the parseSchemaSort describe title — "the header reads what the fetch path reads", falsified by this PR and left standing while two ObjectGrid.tsx comments were corrected — is renamed to say what that block actually pins: the reader's own contract, wider than the fetch path.

The measurement, re-derived on this branch's own base 72bcd7783

Precondition, by commit and ancestry rather than by an API field:

git log origin/main --oneline --grep="(#8758)"                    -> rc 0, 1 match  (9a853f2f3)
git merge-base --is-ancestor 9a853f2f3abc9... origin/main          -> rc 0
git merge-base --is-ancestor <root commit> origin/main             -> rc 0   (control leg)
git rev-parse --is-shallow-repository                              -> false  (fresh full clone)

The control leg matters because a negative --is-ancestor on a shallow checkout is a false negative; this clone is not shallow and the control answers 0, so the positive reading stands on its own.

The divergence itself, on the same base:

  • convertSortToQueryParams (packages/core/src/utils/sort-query.ts:132) refuses a string at :141-144 via reportRetiredSortSpelling (:110-122, an unconditional console.error, deduped per spelling) and returns undefined.
  • packages/plugin-grid/src/ObjectGrid.tsx:1465 takes const schemaSort = schema.sort; and :1852-1859 lowered it privately: a string went verbatim to params.$orderby; an array became a "field order, field order" join string.
  • FIRING CONTROL, same command: 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. So "ObjectGrid is not among them" is a reading, not an assumption.

What changed, and why it is the minimal route-C change

One arm moves. In ObjectGrid's own query memo the string arm no longer assigns $orderby; it calls the shared sink for its refusal alone:

if (typeof schemaSort === 'string') {
  convertSortToQueryParams(schemaSort as unknown as QuerySortEntry[]);
} else if (Array.isArray(schemaSort)) {
  // unchanged
}

Three consequences worth stating explicitly:

  1. No second reporter. reportRetiredSortSpelling is module-private to sort-query.ts, and the dispatch forbids changing that shared file. The only exported route to that one reporter is the sink itself, so the string arm calls it. The diagnostic, its wording and its once-per-spelling dedupe are all feat(core)!: retire the legacy string sort clause — one spelling, the array (objectui#8221) #8758's, unmodified.
  2. The wire shape does not move. The sink's return value is deliberately unused. The array arm still emits this block's own "field order" join string, so the export path (:3046) and the header-arrow reader parseSchemaSort (:4029) read exactly what they read before. Routing the whole key through the sink would send its field-to-direction map where every grid today sends a string; that is the other route, and it is not taken here.
  3. The refusal does not fall through. A refused string still consumes the else if (schemaSort) branch, so it does not silently hand the query to the legacy defaultSort arm. Pinned.

The join-string pins — including a third one the dispatch did not name

The dispatch flagged two live expect(params.$orderby).toBe('name desc') pins and asked which, if any, author a string. Read end to end:

pin authored sort verdict
ObjectGrid.elementDataSource.test.tsx:75 HOT_VIEW.sort = [{ field: 'name', order: 'desc' }] — array untouched, stays green
gridDefaultFiltersLowering.test.tsx:275 defaultSort: { field: 'name', order: 'desc' } — the legacy defaultSort leg, not sort untouched, stays green
gridDefaultFiltersLowering.test.tsx:286 sort: [{ field: 'name', order: 'desc' }] — array untouched, stays green

Neither of the two named pins authors a string. A third one does, and it is not in the dispatch's list:

  • packages/plugin-grid/src/__tests__/serverSorting.test.tsx:141 — renderGrid(ds, { sort: 'name desc' }), asserting expect(lastFindParams(ds).$orderby).toBe('name desc') under the comment "The declared sort goes out as the string form it was authored in." That is a direct pin of the retired lowering at this exact read site — this card's own subject — and 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 to it. So its fixture is re-authored in the declared array spelling (the assertion value 'name desc' is unchanged, because the array arm is unchanged), which keeps its #3106 subject intact; the refusal it used to contradict is pinned separately, with both halves, in a dedicated file. Saying which, rather than editing quietly.

A fourth site authors a string — the header-arrow case in the same file (sort: 'status desc'). The first round left it string-authored with a comment, on the reasoning that this kept the parseSchemaSort-vs-fetch-path divergence visible. ⛔ That was wrong and the rework fixed it (R2 above): its fixture is now the declared array spelling, and the divergence is asserted by a dedicated pin naming #8961 — arrow drawn and $orderby key absent and one diagnostic — instead of being left as a passing test that reads like the state is intended.

Two comments this change falsifies, corrected — comment only

Both are claims that were true before this commit and are not after it. Neither touches behaviour:

  • ObjectGrid.tsx parseSchemaSort docblock: it asserted "this grid's own fetch path already reads all three" spellings. It no longer does. The docblock now names the one declared spelling and records that this reader is wider than the fetch path, and that closing the gap is the other route.
  • ObjectGrid.tsx:4019 (the header-arrow read site): it asserted "the arrow on screen and the $orderby on the wire are the same sort". One spelling now escapes that, and the comment says so.

Ablation — every mutation proven on disk before any result was read

Resolution path first: plugin-grid tests import ../ObjectGrid by relative path, and the root vitest config aliases every @object-ui/* specifier to that package's src. Nothing here resolves through dist, so no build gates these legs — but each mutation is still proven on disk by anchor counts on injected and removed text plus a git hash-object differing from the HEAD blob, and each restoration is proven by blob hash, never by an exit code. Both legs restore from trap ... EXIT INT TERM using absolute paths and git checkout HEAD -- PATH.

HEAD blob of packages/plugin-grid/src/ObjectGrid.tsx at the commit under test: 064b293dcd210d1d62622bc8aff059b59489d80e.

Predictions were recorded before each run.

Leg 1 — delete the refusal (restore params.$orderby = schemaSort;).

PROOF injected('ABLATION_8767_LEG1')=1  removed_text_still_present=0
      hash=fe6a396fb4b0f1abb2c9c017e23531edf791839c  head=064b293dcd...
RESTORE: hash-object=064b293dcd...  head=064b293dcd...  -> identical, git diff HEAD empty

Predicted: the four refusal cases red, the two array CONTROL cases green, the other three files green. Observed, exactly:

gridRetiredStringSort-8767.test.tsx (6 tests | 4 failed)
Test Files  1 failed | 3 passed (4)
     Tests  4 failed | 32 passed (36)

Leg 2 — mutate the ARRAY arm ( ${s.field} ${s.order} becomes ${s.field}:${s.order} ; a separator-only mutation would have been invisible on a single-key sort).

PROOF injected('ABLATION_8767_LEG2')=1  removed_text_still_present=0
      hash=930650a794ab2d40e8ef08444b52bd31b1fb275f  head=064b293dcd...
RESTORE: hash-object=064b293dcd...  head=064b293dcd...  -> identical, git diff HEAD empty

Predicted: the two array CONTROL cases red, both surviving join-string pins red, the re-authored serverSorting case red, the four refusal cases green, and — the asymmetry that makes it a control — gridDefaultFiltersLowering:275 (the legacy defaultSort leg, a different code path) green. Observed, exactly:

gridRetiredStringSort-8767.test.tsx   (6 tests | 2 failed)   <- the two CONTROL cases
gridDefaultFiltersLowering.test.tsx  (15 tests | 1 failed)   <- :286 only; :275 green
serverSorting.test.tsx               (11 tests | 1 failed)
ObjectGrid.elementDataSource.test.tsx (4 tests | 1 failed)
Test Files  4 failed (4)
     Tests  5 failed | 31 passed (36)

Leg 1 says the refusal is what makes the new 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, not a vacuous claim — and that the pin cannot pass by refusing everything.

Verification

Exit codes captured into files before any pipe; heavy runs serialized through the container's shared verify lock and read from its VERDICT line.

run result
pnpm exec vitest run packages/plugin-grid/ exit 0 — Test Files 121 passed (121), Tests 1070 passed (1070)
pnpm exec vitest run on the four sort files, final tree exit 0 — Test Files 4 passed (4), Tests 36 passed (36)
cross-package readers of ObjectGrid.tsx plus sort-query.test.ts and ObjectView.sortSink.test.tsx exit 0 — Test Files 6 passed (6), Tests 50 passed (50)
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
pnpm --filter @object-ui/plugin-grid lint exit 0 — log echoes > @object-ui/plugin-grid@17.6.0 lint / > eslint .; ✖ 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 log
node scripts/check-changeset-presence.mjs exit 0 — 2 published source files changed, 1 changeset declared
pnpm changeset:check exit 0 — fixed group OK, no major declared
pnpm check:control-bytes exit 0 — 7166 tracked text files scanned
node scripts/check-governed-queue-guard.mjs --test on all four changed paths exit 0 — NOT GOVERNED, none of the five governed surfaces matched

Re-run on the rework head 52d28eb4b:

run result
the five sort-affected plugin-grid test files exit 0 — Test Files 5 passed (5), Tests 45 passed (45)
turbo run type-check --filter=@object-ui/plugin-grid --concurrency=2 exit 0 — 14 tasks successful
pnpm --filter @object-ui/plugin-grid lint exit 0 — script name echoed; ✖ 794 problems (0 errors, 794 warnings); zero severity-error lines and zero occurrences of the literal lowercase word error
node scripts/check-changeset-presence.mjs exit 0 — 3 published source files changed, 1 changeset declared
pnpm changeset:check exit 0
pnpm check:control-bytes exit 0 — 7166 tracked text files

⚠️ Declared narrowing on the rework round. The full 121-file plugin-grid suite was not re-run locally on 52d28eb4b. The container's shared verify lock returned exit 99 (never acquired) twice, after 9 minutes of budget each, against a single holder that held it for 23 minutes; the third attempt ran a narrowed set. The narrowing is sound rather than 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.

The one-off probe behind #8973's table ran inside that same lock hold as a temporary test file, and was removed in the same script under a trap ... EXIT INT TERM; its removal is confirmed by the tree being clean of it (git status names only the two intended files).

A first pnpm --filter @object-ui/plugin-grid type-check exited 2 with Cannot find module '@object-ui/core' — that is the dependency dist not yet built, i.e. PREREQUISITE NOT MET, not a red gate. It is recorded here rather than reported as a failed measurement; the turbo run above is the real reading.

Scope

packages/plugin-grid/** plus one .changeset/*.md. packages/core/src/utils/sort-query.ts is read and reused, not changed. The wire shape, the export path, parseSchemaSort's behaviour, its exported signature and object-calendar are all untouched.

Two successor cards carry what this ruling parked, both filed unassigned and ungraded: #8961 (the header reader is wider than the fetch path) and #8973 (the array arm interpolates missing keys into $orderby). They 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 #8973 says so, so triage may fold it into #8961 if it prefers one card.


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3484.1 KB 3512.7 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-oq0q_gl0.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) 16.69KB 6.21KB
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) 500.20KB 114.67KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 28.04KB 9.46KB
data-objectstack (index.js) 205.46KB 56.78KB
fields (index.js) 246.97KB 62.30KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.63KB 19.99KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 132.41KB 34.84KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 253.19KB 65.62KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.65KB 40.91KB
plugin-grid (index.js) 210.87KB 57.30KB
plugin-kanban (index.js) 46.06KB 14.30KB
plugin-list (index.js) 112.54KB 27.65KB
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.42KB 20.80KB
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) 83.34KB 27.61KB
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.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
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) 14.82KB 4.99KB
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

Contract review — PR #8960 / card #8767 — VERDICT: REWORK (two bounded items; the refusal itself is judged correct)

Contract-review seat, subagent agent-a6b18b9bebde4b22e of session session_01Jmxdo7bmeqCQHLSfmLVX9w, 2026-09-10. Everything below was measured on a fresh full clone at head 76a9ead402cc173ce7c9bea00eaf6daed59c2bb9 (base 72bcd7783) under my own scratchpad, never on a shared checkout; ⛔ no file in the PR was edited, no label touched, nothing pushed.

0. Tier fuse (read before any judgement)

  • Single source: /home/user/objectstack/scripts/pm/dispatch-gates.mjs:10176 → export const CONTRACT_REVIEW_TIER = 'claude-fable-5-1'.
  • My transcript was located by a marker only I wrote (CR8960-MARKER-zq7vX4pL9mK2wN8e → subagents/agent-a6b18b9bebde4b22e.jsonl). Harness-stamped "model": values, grepped twice during the round: 38 stamps, then 51 stamps — every one claude-fable-5-1, zero other values. Positive control on the same file: 33 "role":"assistant" / 21 "role":"user". The token fallback occurs 5× — each is system-prompt text, my own grep command, or a CI bundle-table row (fallbackInterpolation.js); none is a harness fallback notice.
  • Reading = CONTRACT_REVIEW_TIER, no downgrade evidence ⇒ this verdict is issued.

1. Independence pair — and this is a SELF-REVIEW

Implemented-by: claude/issue-8767-object-grid-refuses-string-sort (mode:subagent)
Reviewed-by: agent-a6b18b9bebde4b22e (subagent of session_01Jmxdo7bmeqCQHLSfmLVX9w; served model claude-fable-5-1 per transcript stamps)

⚠️ SELF-REVIEW. The implementing subagent (PR body footer, card Claim: line) and this reviewing subagent were both started by the same dispatch session session_01Jmxdo7bmeqCQHLSfmLVX9w. Per contract-review.md 〈复核归属与资格〉 line 37 — 「两者同 session ⇒ 报 SELF-REVIEW,⛔ 不作独立复核」 — this verdict is not an independent second opinion, regardless of context isolation. What it is: a ceiling-tier, transcript-verified reading of the low-tier increment. The parent has two legal moves only (adopt verbatim, or void as a whole); if the dispatch seat wants a genuinely independent opinion it must come from another session. Nothing below is softened on that account.

2. Carrier state — the card is 漏挂 (never hung), not 已清标

3. ① Derived judgements — accept set and public face, measured

(a) What changed, per input class. Probe rendered the real object-grid through SchemaRenderer at HEAD, then again with ObjectGrid.tsx swapped to the base blob 95d8fbba… and restored by hash (064b293d… == HEAD, git diff --quiet rc 0). retiredMsgs = console.error calls containing retired.

input BASE $orderby / msgs HEAD $orderby / msgs changed
"name desc" "name desc" / 0 key absent / 1 YES
"name" "name" / 0 absent / 1 YES
"name DESC", "-name" verbatim / 0 absent / 1 YES
" " (whitespace) " " / 0 absent / 1 YES
"" absent / 0 (falls to defaultSort leg) same –
undefined, key absent, null absent / 0 same –
42, {name:"desc"} absent / 0, silent, and consumes the branch (no defaultSort fallthrough) same –
[] "" / 0 same –
["name desc"] (retired array-of-strings) "undefined undefined" / 0 same –
[{field:"name"}] (order omitted) "name undefined" / 0 same –
[{field:"name",order:"desc"}] "name desc" / 0 same –
[{status asc},{name desc}] "status asc, name desc" / 0 same –
"name desc" + defaultSort:{status asc} "name desc" / 0 absent / 1 (no fallthrough) YES
object-view DEFAULT, table.sort:"name desc" "name desc" / 0 absent / 1 YES ⚠️
object-view viewType:'grid', string "name desc" / 0 absent / 1 YES ⚠️
object-view DEFAULT, array "name desc" / 0 same –

⚠️ The three object-view rows: values were captured, but the probe tests were stamped FAIL by the repo's network-escape guard (objectui#6640 — ObjectView fires a relative security/explain fetch I did not mock). Treat them as readings with that caveat, not as pins.

Verdict on (a): exactly one arm moved — every truthy string — and it moved the same way on the bare grid and through object-view's default/grid path (which delegates to the real ObjectGrid, so the PM correction's "cover the default path" is met by construction). Nothing else moved. The five silent garbage outputs on the array arm ("", "undefined undefined" ×2, "name undefined") are pre-existing and unchanged; they are route B's subject and belong on #8961 (see ③).

(b) Consistent with #8758's sink, or a second semantics? For the string arm there is literally one semantics: the grid calls convertSortToQueryParams itself, so refusal condition (!sort guard then typeof === 'string'), diagnostic text, and once-per-spelling dedupe are the sink's own. Read on the tree: sort-query.ts:132-160. For the array arm two semantics remain (sink → {field:direction} map, drops field-less entries, defaults order to asc; grid → join string, emits undefined), deliberately, per the ruling — not introduced here.

(c) reportRetiredSortSpelling is module-private — confirmed, and the technique is side-effect dependence, not clean reuse. sort-query.ts:109 is function reportRetiredSortSpelling with no export; packages/core/src/index.ts:42 export * therefore exposes only QuerySortEntry, resetRetiredSortSpellingReports, convertSortToQueryParams. So the implementer's claim holds: under "reuse the reporter" + "do not change sort-query.ts" the sink call is the only route. My judgement of the technique: (i) the call computes a value and discards it; (ii) it casts a string through unknown to call a function against its own signature; (iii) the sink's docblock documents the diagnostic as part of its contract, so this is documented behaviour, not an accident — which is why it is tolerable; but (iv) it carries the sink's prescription into a block where the prescription is false: the message says "order is optional and means 'asc'", and on this block [{field:'name'}] lowers to $orderby: 'name undefined' (table above). The grid now prints migration advice that, if followed literally, produces garbage on the grid. That debt must be written on #8961 (R-note below), and the changeset must not repeat the false sentence (R1).

(d) serverSorting.test.tsx:141 rewrite — does it mask a regression? Read at base and head. The case's assertions are: pre-click $orderby equals 'name desc', post-click equals [{status asc}] (replace, not stack — objectui#3106's subject). The only assertion the PR would have turned red is the pre-click one, and only because the retired lowering is the card's own subject; the array fixture yields the identical string, the #3106 assertion is still measured, and the inverse (string → no $orderby) is pinned in the new file. Not a masked regression; disclosed correctly.

⛔ But the same file at :170 (sort: 'status desc', "shows the view's declared sort before anyone clicks") is where a defect is now frozen green. By the implementer's own principle for :141 — subject is the header behaviour, spelling incidental — this fixture's subject is the arrow, so it too belongs in the declared spelling. Left string-authored, it is a passing test that certifies: string sort ⇒ arrow shown, wire unordered, one console.error. The in-place comment says this keeps the divergence "visible"; a green test does not make anything visible — it certifies the state as expected. The describe title at :185, "the header reads what the fetch path reads", is now false too, and the PR corrected two falsified comments in ObjectGrid.tsx while leaving this one. → R2.

(e) New pin gridRetiredStringSort-8767.test.tsx. Live subject: $orderby key absent (hasOwnProperty false, not just undefined), diagnostic quotes the value / array form / objectui#8221, once per spelling across two mounts, no fallthrough to defaultSort. Controls are real: exact string equality 'name desc' and 'status asc, name desc' plus errorSpy.not.toHaveBeenCalled() — a refuse-everything implementation fails both (absent key ≠ string), a route-B map fails both (object ≠ string). The PR's leg-2 ablation reports exactly that; my run of the seven related files at head: 7 files / 82 tests, exit 0. Gap, not blocking: no object-view grid-default leg, which os-bill's correction asked for and the ruling's appetite did not require.

(f) #8961 (arrow says desc, wire carries nothing). Introduced by this PR? Yes: at base arrow and wire agreed for a string-authored grid; at head they disagree. Blocking? No. The ruling names parseSchemaSort as a second private lowering and parks it explicitly; the state is loud (one console.error naming the spelling), bounded to documents still carrying the retired spelling, and the export path (ObjectGrid.tsx:3078, Array.isArray(schemaSort) ? … : undefined, untouched) was already dropping strings before this PR — the fetch path now agrees with export. Acceptable as a successor card.

4. ② Semver

Changeset: '@object-ui/plugin-grid': minor with "Breaking, deliberately" in the body. Precedent, read on the tree: #8758's own changeset (squash 9a853f2f3, .changeset/8221-retire-legacy-string-sort.md) scored @object-ui/core: minor for a narrowing and wrote the break in the body; AGENTS.md 版本号策略 :240 — objectui's own breaking changes are minor, the break stated in the body, enforced by scripts/check-changeset-no-major.mjs; plugin-grid is in the one fixed group (40 members); CI Changeset Bump Policy green on this head. Level is correct. The body has two false sentences → R1.

5. ③ Boundary flags — each answered

6. VERDICT: REWORK

The refusal, its control legs, the semver level and the successor card are judged correct. Two bounded items, neither reopening the ruling:

R1 — changeset body, two factual corrections (.changeset/8767-object-grid-refuses-string-sort.md):

  • (a) "the only one @objectstack/spec accepts" is false on the grid face: @objectstack/spec@17.4.0 ui.ObjectGridPropsSchema.safeParse PARSES sort: "name desc", "name", 42, ["name desc"], {name:"desc"} and REFUSES bogusProp (control). Say instead that the spec's grid props face is value-agnostic on sort, and the array is the one spelling @object-ui/types declares and the sink lowers.
  • (b) "(order is optional and means 'asc')" is false here twice over: SortConfig.order is required (packages/types/src/objectql.ts:327, zod mirror objectql.zod.ts:142), and this block emits 'name undefined' when it is omitted. Drop the parenthetical or state the truth for this block.

R2 — packages/plugin-grid/src/__tests__/serverSorting.test.tsx:

Dispatch-seat actions surfaced, not REWORK items: hang needs:contract-review on card #8767 (§2); append the four array-arm findings to #8961 (§5).

7. NOT measured

  • The full plugin-grid suite (121 files) and any cross-shard run — I ran 7 targeted files / 82 tests; CI's 33 check runs on 76a9ead were read (all completed, 0 failures, 3 skipped: dependabot, Test coverage ×2) but not re-run.
  • type-check and lint — not run here; read from CI only.
  • The three object-view probe rows carry the network-escape caveat above.
  • check-widening-tells.mjs — not applicable (Clause-②: yes).
  • ListView.parseSortConfig (plugin-list) — read, untouched, outside the ruling's appetite; not exercised.
  • Browser behaviour; dist build (pnpm install --offline --ignore-scripts, tests resolve via the root vitest src aliases).
  • Whether finding(plugin-grid): after #8767 the header arrow still reads the retired string sort spelling the fetch path now refuses — parseSchemaSort is wider than the wire #8961's de-dup search was exhaustive.

Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

⛔ Landing withheld — REWORK adopted, plus the carrier the review caught me missing

PM seat (domain:spec, session session_01Jmxdo7bmeqCQHLSfmLVX9w). Disposition of the contract review at 5618090408, whose VERDICT: REWORK I adopt verbatim. ⛔ Nothing below rewrites, abridges or softens it.

All 33 check runs are green on 76a9ead40 (30 success, 3 skipped, 0 failing, 0 pending, read via REST). Green is landing condition ③, not landing. ① is a contract-tier PASS on record, and there is none.

The carrier defect is mine

The review measured that needs:contract-review was never applied to card objectui#8767 — only to this PR, at 11:05:24Z — and that PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 8960 therefore exits 4 (row C1), not 0. It correctly read that as 漏挂, not 已清标: there is no PASS on either carrier and the head has not moved, so the absent label is a missing hang, not a clearance.

That is the dispatching seat's to fix, and I have hung it on the card. ⛔ It stays on both carriers until a PASS against the repaired head.

Route C itself is not in question — and I re-measured why

The review's R1(a) is right that the changeset's sentence is false, and the reason matters enough to state precisely, because the maintainer principle now in force («我们的项目以 objectstack 协议为准», relayed on objectui#8934 5617613810) makes "does this go narrower than the protocol?" a blocking question for any accept-set narrowing.

Measured in this seat, objectstack-ai/objectstack HEAD eabdd66f45f402eba0f8404a8a9de4a501fc83a6:

where what it says
shared/enums.zod.ts:27-30 SortItemSchema = z.object({ field: z.string(), order: SortDirectionEnum }) — the protocol's reusable sort item. order carries no .optional().
ui/component.zod.ts:2231 ElementRecordPickerPropsSchema.sort: z.array(SortItemSchema).optional() — the protocol's typed spelling of this exact intent, on a sibling block
ui/component.zod.ts:2470 ObjectGridPropsSchema.sort: z.unknown().optional().describe('Initial sort (array of { field, order })')
ui/component.zod.ts:2489-2498 the defaultSort retirement: "The live mechanism is sort — the same pair, wrapped in an array"

⇒ z.unknown() on object-grid.sort is the protocol not policing the field, not the protocol blessing the string. Its own describe, its sibling's typed declaration, and the defaultSort retirement text all name the array of { field, order }. Route C moves toward the protocol, not away from it. The principle does not bite here.

But that is exactly why the changeset sentence has to be repaired rather than defended: "the only one @objectstack/spec accepts" is a claim about the validator, and the validator accepts the string. The true sentence is a claim about what the protocol declares.

R1(b) turned up a second defect, in code this PR does not touch

SortItemSchema.order is required, so "order is optional and means 'asc'" is false — and the review measured that the untouched array arm emits $orderby: 'name undefined' when order is omitted. That is a real latent defect and it is not this PR's to fix: widening the diff to reach it is what the ruling refused. It is owed a successor card, alongside objectui#8961.

R2 — the frozen-defect leg

The review is right, and it is right by the implementer's own stated principle. serverSorting.test.tsx:141 was re-authored in the array form precisely so a pin would stop asserting something the wire no longer does; :170 was then left authoring the retired string, green, while the wire carries no ordering — the same shape, kept. Leaving the divergence visible was the correct instinct; leaving it visible as a green test that asserts only the arrow is not the way. It becomes an explicit pin of the divergence, named for objectui#8961. The now-false describe title at :185 is unambiguous and goes with it.

What I am NOT asking for

⛔ Do not touch the refusal mechanism, the control pins, the minor grading, or objectui#8961's parking — the review judged all four correct and I am not reopening them. ⛔ Do not widen the diff to the array arm's undefined bug.

SELF-REVIEW

The review declares SELF-REVIEW: reviewer and implementer are both subagents of this one dispatching session, so context isolation is not independence. Recorded here rather than treated as an independent clearance — and acted on in the stricter direction (withholding a landing), which is the direction SELF-REVIEW is safe in.

PR stays open and draft.


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 50 chunks) 3484.1 KB 3512.7 KB
Main entry chunk (gzip) 144.2 KB 350 KB
Entry file index-DPdiJ0P1.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) 16.69KB 6.21KB
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) 500.20KB 114.67KB
core (index.js) 7.48KB 2.96KB
create-plugin (index.js) 28.04KB 9.46KB
data-objectstack (index.js) 205.46KB 56.78KB
fields (index.js) 247.15KB 62.32KB
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) 14.81KB 3.63KB
plugin-calendar (index.js) 49.03KB 13.93KB
plugin-charts (index.js) 71.63KB 19.99KB
plugin-chatbot (index.js) 195.32KB 46.51KB
plugin-dashboard (index.js) 132.41KB 34.84KB
plugin-designer (index.js) 215.68KB 44.27KB
plugin-detail (index.js) 253.19KB 65.62KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.79KB 34.19KB
plugin-gantt (index.js) 166.65KB 40.91KB
plugin-grid (index.js) 210.87KB 57.30KB
plugin-kanban (index.js) 46.06KB 14.30KB
plugin-list (index.js) 112.54KB 27.65KB
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.42KB 20.80KB
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) 83.34KB 27.61KB
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.66KB 2.50KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (kanban-quick-add.js) 3.89KB 1.87KB
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) 14.82KB 4.99KB
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

Contract review

Round 2 (delta) — PR #8960 / card #8767 — judged head 52d28eb4b4551fffe3ffc9a46fde2d532fc99785 (base 72bcd7783a473bd7bdd2884c21787187f3214c65; previous record 5618090408 judged 76a9ead402cc173ce7c9bea00eaf6daed59c2bb9 and no longer covers this head). Contract-review seat, subagent agent-a6b18b9bebde4b22e of session session_01Jmxdo7bmeqCQHLSfmLVX9w, 2026-09-10. Measured on my own scratchpad clone, checked out detached at the head above; ⛔ no PR file edited, no label touched, nothing pushed, nothing approved.

0. Tier fuse, re-read this round

  • /home/user/objectstack/scripts/pm/dispatch-gates.mjs:10176 → export const CONTRACT_REVIEW_TIER = 'claude-fable-5-1'.
  • Transcript subagents/agent-a6b18b9bebde4b22e.jsonl (marker CR8960-MARKER-zq7vX4pL9mK2wN8e, 6 hits): harness-stamped "model": values grepped at the start of this round — 101 stamps, all claude-fable-5-1, one distinct value. Residual fallback tokens are system-prompt text (font stacks, get_session docs); no harness fallback notice.
  • Reading = CONTRACT_REVIEW_TIER ⇒ this record is issued.

1. Independence pair — SELF-REVIEW

Implemented-by: claude/issue-8767-object-grid-refuses-string-sort (mode:subagent)
Reviewed-by: agent-a6b18b9bebde4b22e (subagent of session_01Jmxdo7bmeqCQHLSfmLVX9w; served model claude-fable-5-1 per transcript stamps)

SELF-REVIEW. Implementer and reviewer are subagents of the same dispatching session. Per contract-review.md 〈复核归属与资格〉 line 37 this is not an independent second opinion; it is a ceiling-tier, transcript-verified reading. The parent's legal moves are adopt-verbatim or void-as-a-whole. Nothing below is softened on that account.

2. Scope of this round, and what did NOT move

git diff 76a9ead4…52d28eb4 names exactly two files: .changeset/8767-object-grid-refuses-string-sort.md and packages/plugin-grid/src/__tests__/serverSorting.test.tsx. Blob proof that the four things judged correct last round are untouched: ObjectGrid.tsx 064b293dcd210d1d62622bc8aff059b59489d80e (both heads), gridRetiredStringSort-8767.test.tsx 555c67b3… (both), packages/core/src/utils/sort-query.ts f01f6d1a… (both), changeset frontmatter still '@object-ui/plugin-grid': minor. The refusal mechanism, the control pins, the minor grading and #8961's parking are therefore not reopened — they are byte-identical to what was judged.

3. ① Derived judgements — the delta, item by item

R1(a) — the sentence "the only one @objectstack/spec accepts" is gone; what replaced it is true on the tree. The changeset now separates what the validator accepts from what the protocol declares, and every fact it rests on re-derives:

claim in the new text reading
ui.ObjectGridPropsSchema.sort is z.unknown().optional(); safeParse accepts string / 42 / ['name desc'] / {name:'desc'}; bogusProp refused unrecognized_keys installed @objectstack/spec@17.4.0 dist: sort: z.unknown().optional().describe("Initial sort (array of { field, order }) — measured last round, control refused
the key's own describe reads Initial sort (array of { field, order }) ObjectGridPropsSchema.sort.description = exactly that string (dist), and objectstack packages/spec/src/ui/component.zod.ts:2470
sibling ElementRecordPickerPropsSchema.sort is a typed z.array(SortItemSchema) that refuses a string outright component.zod.ts:2231; measured with a correct fixture ({object:'account', sort}): array PARSES, 'name desc' REFUSED sort:invalid_type, ['name desc'] REFUSED sort/0:invalid_type, control bogusProp REFUSED unrecognized_keys
the defaultSort retirement text says rename the key to sort and wrap the value in an array component.zod.ts:2492-2496, verbatim: "Rename the key to sort and wrap the value in an array (defaultSort: { field, order } becomes sort: [{ field, order }])"
the registered sort input publishes type: 'array' packages/plugin-grid/src/index.tsx:222: { name: 'sort', type: 'array', description: 'Initial sort order, [{ field, order }]…' }
ObjectGridSchema.sort declares only the array since #8221; convertSortToQueryParams lowers only the array packages/types/src/objectql.ts:701 sort?: SortConfig[]; sort-query.ts:132-160

The text also says plainly that "this change is not a narrowing the validator already performed" — correct, and it keeps route C's justification where it belongs: the protocol's declared shape, which is what the maintainer's ruling rests on («Keep the wire shape; make ObjectGrid refuse a string schema.sort with the same diagnostic, before its own lowering.»). Judged true.

R1(b) — "order is optional and means 'asc'" is gone; "Both keys are required" is true, and the garbage output is filed. SortConfig.order required: objectql.ts:327 (no ?), objectql.zod.ts:142 (no .optional()). Protocol SortItemSchema (shared/enums.zod.ts:27-30, order: SortDirectionEnum): measured {field:'name'} → REFUSED order:invalid_value; {field,order:'desc'} → PARSES; {field,order:'up'} → REFUSED order:invalid_value (control). The successor card is #8973 (opened 12:13:33Z, unassigned). Its six-row table is reproducible: I re-ran all six through the real object-grid at 52d28eb4… — [{name,desc}] (control) → "name desc"; [{name}] → "name undefined"; [{name,desc},{status}] → "name desc, status undefined"; [] → ""; ['name desc'] → "undefined undefined"; [{order:'desc'}] → "undefined desc". Same values as my base/old-head probe last round, as expected from the identical ObjectGrid.tsx blob. Judged true and correctly parked.

R2 — the three steps, each verified in the diff and by ablation.

  • :170 fixture is now [{ field: 'status', order: 'desc' }]; the arrow assertion and the click-to-asc assertion are unchanged.
  • The new case "DIVERGENCE, pinned not tolerated (objectui#8961)…" asserts both halves: chevron-down present, Object.prototype.hasOwnProperty.call(params, '$orderby') === false, and exactly one console.error containing objectui#8221, with resetRetiredSortSpellingReports() before and mockRestore in finally. Proof each half is live, ObjectGrid.tsx mutated in my clone and restored by hash (064b293d… == HEAD, git diff --quiet rc 0, both legs):
    • Leg A (swap to the base blob 95d8fbba…, i.e. delete the refusal): serverSorting.test.tsx → 1 failed / 11 passed — the divergence pin alone goes red (wire half live); the re-authored :170 stays green.
    • Leg B (blind parseSchemaSort's string arm, [sort] → [], injected marker 1 / removed literal 0): → 3 failed / 9 passed — the divergence pin plus the two bare-string parseSchemaSort unit cases (arrow half live).
  • The describe at :185ff is renamed to "parseSchemaSort — the header reader's own contract, wider than the fetch path (objectui#8961)" with a docblock saying the block pins the reader's inputs, never the grid's query. Accurate.

Regression surface. Seven related files at 52d28eb4…: 7 files / 83 tests, exit 0 (82 → 83 = the one new case). CI on this head: 33 check runs, 30 success, 3 skipped (dependabot, Test coverage ×2), 0 failures, 0 in progress. Read-only git merge-tree --write-tree origin/main 52d28eb4… (origin/main now 7 commits past the base; bare clone, no os-regen driver registered): rc 0, tree 153adc20…, no conflict. Nothing judged correct last round was disturbed.

4. ② Semver — delta

Frontmatter unchanged, '@object-ui/plugin-grid': minor; Changeset Bump Policy and Changeset Fixed Group Check green on this head. Level still correct (AGENTS.md 版本号策略 :240; #8758 precedent). Body now carries no false contract sentence that I could find.

5. ③ Boundary flags — delta

6. Carrier state on this head

Timelines (REST): card #8767 — needs:contract-review labeled 11:42:26Z, unlabeled 11:48:07Z, labeled 12:17:22Z; PR #8960 — labeled 11:05:24Z, unlabeled 11:48:11Z, commit 52d28eb4b 12:12:45Z, labeled 12:17:23Z. Both carriers hung, one second apart, after the head moved. PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/check-clause2-carriers.mjs --pair 8960 → exit 0, "both carriers agree" (script version: objectstack eabdd66f, reported as read). The head moved after record 5618090408, so that record does not count; this comment is the record on 52d28eb4b4551fffe3ffc9a46fde2d532fc99785. ⛔ I touch no label; clearing is the dispatch seat's stroke.

7. NOT measured this round

VERDICT: PASS


Generated by Claude Code

@os-warren
os-warren marked this pull request as ready for review September 10, 2026 15:01
@os-warren
os-warren added this pull request to the merge queue Sep 10, 2026
Merged via the queue into main with commit c5faf06 Sep 10, 2026
35 checks passed
@os-warren
os-warren deleted the claude/issue-8767-object-grid-refuses-string-sort branch September 10, 2026 15:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants