Skip to content

refactor(components): bind SortDirection to @objectstack/spec (objectui#7265 components slice) - #9524

Merged
os-tesla merged 1 commit into
mainfrom
claude/issue-7265-components-sort-direction
Sep 14, 2026
Merged

os-tesla merged 1 commit into
mainfrom
claude/issue-7265-components-sort-direction

Conversation

@os-tesla

Copy link
Copy Markdown
Collaborator

Part of objectui#7265 — the @object-ui/components slice of the check:spec-symbols DEBT burn-down. Two groups remain (@object-ui/data-objectstack / normalizeFilterOperator, @object-ui/plugin-detail / RecordAlertProps), so the card must stay open and Part of is used deliberately.

The site, and what it really was

packages/components/src/renderers/complex/data-table.tsx declared, module-local:

type SortDirection = 'asc' | 'desc' | null;

Measured at the RESOLVED pin — @objectstack/spec@17.4.0, the version pnpm-lock.yaml resolves and node_modules installs, not the 17.2.0 the seeding card asserted against — the spec's SortDirection is z.input of SortDirectionEnum on the @objectstack/spec/shared subpath, and its members are asc and desc. Nothing else. So the third state was real and the RENAME question was genuinely live.

Route: BIND, and the third state decided it — the other way

null is not a third direction; it is the absence of one. Three readings at the site settled it:

  • It is written at exactly one place — the end of the client-side header cycle in handleSort — and read only by guards (if (!sortColumn || !sortDirection), and the activeSort ternary) that guard sortColumn in the same breath.
  • sortColumn, the other half of the same state pair, already spelled its own empty case at the slot (a useState of string-or-null) instead of folding it into a type name. Folding this half into a type name was the odd one out, not the house spelling.
  • The non-null half flows verbatim into a TableSortItem, whose order is the same two-member vocabulary. Those members must track the protocol; a RENAME would have left them hand-written under a name rule 1 can no longer match — the objectui#4592 hole the gate's own header records.

So the strongest BIND form was available and is what landed: the type is imported outright, the local declaration is gone rather than re-derived, and the divergence is confined to the one state slot that carries it, with the reason written there. applySort held a second inline hand copy of the same two members in its signature and now takes the bound type too.

ALLOW gained nothing: there is nothing here that deliberately differs from the spec's direction vocabulary. That absence is itself pinned, so a later hand that waives it instead has to delete an assertion.

The ledger, before and after

Both readings are the gate's own banner, on this branch:

before   ✅  spec symbol derivation: 1384 files scanned against 5085 spec export names; 18 declared dialects, 3 untriaged collisions in 3 packages.
after    ✅  spec symbol derivation: 1384 files scanned against 5085 spec export names; 18 declared dialects, 2 untriaged collisions in 2 packages.

The block was regenerated with node scripts/check-spec-symbol-derivation.mjs --ledger, never hand-edited. Rule 2's ledger is untouched (18 unbacked claims in 5 packages on both readings) — the declaration this slice deleted carried no spec-alignment claim.

An intermediate reading, taken deliberately, shows the site edit is real and not a ledger edit wearing its clothes — site fixed, block not yet regenerated:

exit 1 — @object-ui/components lists 1 symbol in DEBT that no longer collide — `SortDirection`.

Ratchets, with both readings from real runs

scripts/__tests__/spec-symbol-ledger-components-7265.test.ts (new) — the site half runs the gate's own scanFile over the real file; the block half asserts shrink-only. Sibling of the core / app-shell / types ledger tests.

Ablation, on the committed tree: the site and the gate script reverted to the pre-slice blobs, the new test run unchanged.

site declares the local alias: 0 -> 1     (mutation proven on disk, both directions)
site imports the spec type:    1 -> 0
DEBT lists the group:          0 -> 1

FAIL  rule 1 no longer sees `SortDirection` -- the renderer imports it now
FAIL  DEBT no longer lists `SortDirection`
FAIL  the whole `@object-ui/components` group is gone
FAIL  DEBT has not grown -- 2 names is the ceiling this slice left
Test Files  1 failed (1)      Tests  4 failed | 6 passed (10)

Restored from HEAD and verified by blob hash, not by an exit code: both paths back to d992df3 / 6e76953, git diff HEAD empty.

After restore, on the slice: Test Files 3 passed (3) · Tests 91 passed (91) for the scripts-side ledger suites, and Test Files 7 passed (7) · Tests 200 passed (200) across every scripts/__tests__ suite that names the edited gate.

The scanner is only evidence if it can fail, so the site assertion is paired with both controls: a throwaway fixture carrying the exact deleted declaration, which it MUST flag, and the derived union-extension spelling, which it must NOT — the second one also records what the gate does and does not decide. The gate is indifferent between the outright import and a derivation; the site chose, not the gate.

packages/components/src/__tests__/share-filter-sort-spec-parity.test.ts (appended, not duplicated — this package already owns a spec-symbol parity file from batch 5). The old declaration is gone, so there is no local name left to reach for; what is pinned is the SPEC-side property the binding rests on, the way the app-shell slice pinned its own module-local names. Its batch-5 census is left alone and the arrival is appended under its own card.

Two ablations prove those lines are compiled rather than commentary, and that the confinement is load-bearing:

(a) drop the confined `| null` from the state slot
    TS2345 Argument of type 'null' is not assignable to parameter of type '"desc" | "asc" | (() => "desc" | "asc")'
    TS2345 Argument of type 'null' is not assignable to parameter of type 'SetStateAction of "desc"|"asc"'    (at the cycle's third click)
    tsc exit 2

(b) flip the `null is not a direction` pin to the opposite claim
    TS2344 Type 'false' does not satisfy the constraint 'true'
    tsc exit 2

Note what (a)'s message contains: the parameter type it names IS the spec's two-member union, which is the binding having resolved. Both legs restored and verified by blob hash.

The behaviour the | null exists for is already pinned and is cited by name rather than re-tested: leaves client-side sorting exactly as it was and never asks for "no sort" — the third click returns to ascending, both in data-table-manual-sorting.test.tsx.

Clause-②: still no, and measured rather than asserted

@object-ui/components is a published package, so the export surface was measured, not reasoned about. Three builds of the package: HEAD, HEAD again (the same-source control), and HEAD with the site reverted.

                                     .d.ts (209)   JS (2)   all emitted (212)
CONTROL  head vs head, same source      0 / 0        0 / 0        0 / 0      (raw / normalised-sorted-line)
SUBJECT  pre-slice vs slice             0 / 0        0 / 0        0 / 0

SortDirection appears in zero emitted files on both sides — the declaration was never exported, so nothing published moves and nothing an exported type accepts moves.

Worth recording as a departure from the dispatch's expectation: it warned that a raw dist diff is not an instrument, measured on packages/types where two builds of byte-identical source differ in union-member and object-key order. The same-source control says that does not hold in @object-ui/components — this package's emit is byte-stable, run to run. The normalised comparison was taken anyway and agrees; both readings are above, so the raw zero is corroborated rather than trusted.

That measurement is also what backs the empty-frontmatter changeset: the emitted JavaScript is byte-identical, so the declaration that this releases nothing is a reading, not a claim. node scripts/check-changeset-presence.mjs accepts it explicitly.

Checks run on this branch

check reading
check:spec-symbols exit 0, banner above
@object-ui/components full suite Test Files 275 passed (275) · Tests 2682 passed (2682)
@object-ui/components type-check exit 0 (tsc --noEmit and tsc -p tsconfig.test.json)
@object-ui/components lint exit 0 — 0 errors, 960 warnings, the package's standing baseline
eslint on the touched files 0 errors; data-table.tsx at 34 warnings before AND after, same file, same config
check:control-bytes · check:new-line-citations · check:test-path-roots · check:changeset-claims · check:phantom-deps · check:self-import · check:unreferenced-sources · check:installed-pin-claims exit 0
scripts/check-changeset-presence.mjs exit 0, empty frontmatter accepted
scripts/check-governed-queue-guard.mjs --test NOT GOVERNED, 5 paths checked

check:spec-floors was NOT measured here: it refuses to run without a whole-workspace build (no-artifact: Build the workspace before running this gate). That is a prerequisite not met, not a red gate, and nothing in this diff moves a version range. Left to CI. Type-aware linting is not enabled in eslint.config.js (no project / projectService), so this diff cannot move the verdict on any file it does not touch — which is what makes the targeted eslint reading above a narrowing rather than a gap.

Acceptance notes — seen, deliberately not addressed here

  • @object-ui/types' TableSortItem.order is a hand-declared 'asc' | 'desc'. That is a documented decision of that package (it takes no dependencies), not a defect, so it is left alone — but the new type pin TableSortItem['order'] equals the spec direction is the tripwire on it, and it reds on the day the spec grows a member.
  • The comment above CLAIM_DEBT_ISSUE in the edited gate carries a figure written by an earlier slice of this same card, and it has been overtaken by the slices since. Not repaired here: the repair is to delete the figure, which is a rewrite of another slice's paragraph, and re-deriving it to "correct" it is the move AGENTS.md 完善设计器的每一个细节 #9 forbids. Noted, not filed.

Generated by Claude Code

…ui#7265)

The DataTable renderer declared `SortDirection` module-locally, under the exact
name `@objectstack/spec/shared` exports — the planted-premise class that
`check:spec-symbols` exists to stop. The third member it carried, `null`, made
the route question live: measured at the resolved pin @objectstack/spec@17.4.0,
the spec's `SortDirection` is the two-member direction and nothing else.

Reading the site settled it toward BIND rather than RENAME. `null` is not a
third direction, it is the absence of one: written at exactly one place (the end
of the client-side header cycle in `handleSort`) and read only by guards that
guard `sortColumn` in the same breath — and `sortColumn`, the other half of the
same state pair, already spelled its own empty case as `useState<string | null>`
at the slot rather than folding it into a type name.

So the strongest BIND form applied: the type is imported outright, the local
declaration is gone rather than derived, and the third state is confined to the
one state slot that carries it with the reason written there. `applySort` held a
second inline hand copy of the same two members and now takes the bound type.

Type-only: an `import type`, a deleted alias and an erased `useState` type
argument. The declaration was never exported, so nothing published moves.

Ledger regenerated with `--ledger`; the block shrank by this package's group.
Ratchets: the spec-side properties the binding rests on are appended to this
package's existing spec-symbol parity file, and the site plus the block are
pinned in scripts/__tests__/spec-symbol-ledger-components-7265.test.ts.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 329 chunks) 3062.6 KB 3104.5 KB
Main entry chunk (gzip) 145.7 KB 350 KB
Entry file index-CRztz5Ax.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) 544.93KB 130.50KB
core (index.js) 8.52KB 3.41KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 215.99KB 59.96KB
fields (index.js) 249.20KB 62.88KB
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) 8.87KB 3.64KB
i18n (index.js) 5.22KB 2.26KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 32.15KB 10.49KB
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.83KB 10.95KB
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.99KB 14.24KB
plugin-charts (index.js) 71.33KB 19.90KB
plugin-chatbot (index.js) 195.34KB 46.51KB
plugin-dashboard (index.js) 131.44KB 34.65KB
plugin-designer (index.js) 215.94KB 44.33KB
plugin-detail (index.js) 252.48KB 65.61KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 136.71KB 34.16KB
plugin-gantt (index.js) 167.62KB 41.26KB
plugin-grid (index.js) 212.83KB 57.97KB
plugin-kanban (index.js) 46.63KB 14.53KB
plugin-list (index.js) 112.67KB 27.68KB
plugin-map (index.js) 21.48KB 6.99KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 43.41KB 11.93KB
plugin-timeline (index.js) 30.07KB 8.74KB
plugin-tree (index.js) 10.58KB 3.72KB
plugin-view (index.js) 84.36KB 20.78KB
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) 99.04KB 32.62KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.63KB 2.18KB
react (schema-input.js) 4.25KB 2.04KB
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) 25.28KB 7.80KB
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.04KB 5.36KB
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

@os-tesla
os-tesla marked this pull request as ready for review September 14, 2026 19:11
@os-tesla
os-tesla added this pull request to the merge queue Sep 14, 2026
Merged via the queue into main with commit 5b673b8 Sep 14, 2026
38 checks passed
@os-tesla
os-tesla deleted the claude/issue-7265-components-sort-direction branch September 14, 2026 19:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants