Skip to content

docs(plugin-list): correct the stale sort advice and the misattributed docblock (#4559) (#4966) - #5615

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-4559-listview-comment-pair
Aug 21, 2026
Merged

docs(plugin-list): correct the stale sort advice and the misattributed docblock (#4559) (#4966)#5615
os-sales merged 1 commit into
mainfrom
claude/issue-4559-listview-comment-pair

Conversation

@os-sales

Copy link
Copy Markdown
Collaborator

Fixes#4559
Fixes#4966

A triage-designated pair: two comment corrections in the same file, same class, shipped
in one PR. Prose only — zero non-comment lines change.

All coordinates were re-anchored on the declarations rather than the line numbers in the
cards, because #4965 (#3950) moved this region and moved UNSORTABLE_FIELD_TYPES out of
the file entirely (it now lives in @object-ui/core as UNMATERIALIZED_FIELD_TYPES).
Line numbers below are the ones actually found on main at 6606337e3.

Edit 1 — #4559: the sort rationale stopped prescribing a formula field

packages/plugin-list/src/ListView.tsx, in the comment block above the
sortHasRelationalField / sortFields memo, lines 2309-2310.

Before:

 // below points at the supported alternative (a formula field that
// denormalizes the name onto this object, which sorts like any text column).

After:

 // below points at the supported alternative (a stored field that
// denormalizes the name onto this object, written when the source changes).

Both claims verified at source before writing, as the card required

The hint string, quoted as it reads today (ListView.tsx:608-609, the provider-less
fallback table):

Columns that link to another record are not listed: they can only be sorted by the
stored ID, not by the name shown in the cell. To sort by that name, denormalize it onto
this object as a stored field, written when the source changes, and sort by that. Not
a formula field
: it is virtual, so no column is stored for it and the server refuses
to sort by one.

The filtering predicate, quoted as it reads today (ListView.tsx:2338):

if(!relational&&!UNMATERIALIZED_FIELD_TYPES.has(field.type)){

UNMATERIALIZED_FIELD_TYPES (packages/core/src/utils/unmaterialized-fields.ts) is
bound to SEARCH_VIRTUAL_TYPES from @objectstack/spec/data, which is
new Set(['formula']) (packages/spec/src/data/search-fields.ts:77, pinned by that
package's own test). So the memo does withhold formula, the string does refuse it, and
the parenthetical was still recommending it. Neither had changed in a way that would
make the parenthetical correct, so the edit proceeded.

The new wording is the one the hint and packages/plugin-list/README.md:229-232 already
share ("a stored field, written when the source changes"), and
"which sorts like any text column" is dropped, as the card asked.

Edit 2 — #4966: formatActionLabel's docblock now sits above formatActionLabel

Same file. The block moved from lines 278-281 (where it stacked above
parseSortConfig's own docblock) down to lines 309-312, directly above
formatActionLabel at line 313.

Before:

/** * Format an action identifier string into a human-readable label. * e.g., 'send_email' → 'Send Email' *//** * Normalize a view's `sort` declaration to SortItem[]. … */exportfunctionparseSortConfig(sort: unknown): SortItem[]{}functionformatActionLabel(action: string): string{}

After:

/** * Normalize a view's `sort` declaration to SortItem[]. … */exportfunctionparseSortConfig(sort: unknown): SortItem[]{}/** * Format an action identifier string into a human-readable label. * e.g., 'send_email' → 'Send Email' */functionformatActionLabel(action: string): string{}

This one turned out to be more than tidiness, and the measurement below is why.
Because parseSortConfig is exported, vite-plugin-dts copied the misattributed block
straight into dist/ListView.d.ts — so every consumer's editor hover and TypeDoc
introduced the sort parser with a sentence about action labels. The card predicted the
adjacency cost to the next reader; it also shipped.

The diff is comment-only, verified rather than asserted

git diff --numstat is 6 6 packages/plugin-list/src/ListView.tsx (plus the changeset).
Every added and removed line, with the +/- stripped, filtered for anything that is
not comment syntax:

git diff -U0 -- packages/plugin-list/src/ListView.tsx \
| grep -E '^[+-]' | grep -vE '^(\+\+\+|---)' | sed -E 's/^[+-]//' \
| grep -vE '^[[:space:]]*(//|/\*\*|\*/|\*([[:space:]]|$))'

returns 0 lines. No strings, no code, no tests — and no test or ablation is possible
on a comment edit, so none is offered rather than manufactured.

Changeset form: a real patch, decided by measurement

The dispatch note's stated mechanism does not hold for this package, and the measurement
says so. packages/plugin-list is not built by plain tsc — its build is vite build
(rolldown for the bundle, vite-plugin-dts for the types); tsc appears only as
--noEmit in type-check, so tsconfig.base.json's removeComments: false never
governs this package's emitted JS.

Measured with the package's real build, before and after the edit:

artifactresult
dist/index.jsbyte-identical, sha256 639a0310d7ab0b88… both sides
dist/index.umd.cjsbyte-identical, sha256 d9db44da63c753a5… both sides
dist/ListView.d.tschanges — the misattributed block is removed
dist/ListView.d.ts.mapchanges (follows the above)

The bundler strips these comments entirely: grep -c "Format an action identifier" over
both emitted bundles returns 0. The .d.ts delta is exactly:

 export declare function normalizeFilterCondition(condition: any[]): any[];
/**
- * Format an action identifier string into a human-readable label.- * e.g., 'send_email' → 'Send Email'- */-/**
* Normalize a view's `sort` declaration to SortItem[]. @objectstack/spec

Consumer-visible API documentation changes, so this gets a real patch changeset rather
than an empty frontmatter. Edit 1 lives inside a function body and reaches no .d.ts;
formatActionLabel is module-private, so its now-correct docblock does not appear there
either — the whole .d.ts delta is the removal above.

Gates

Re-derived from .github/workflows/ against the actual diff rather than taken from the
dispatch list. Every exit code was captured before any pipe; each verdict line is the
gate's own.

gateexitits own verdict line
pnpm --filter @object-ui/plugin-list type-check0ran tsc --noEmit && tsc -p tsconfig.test.json, no diagnostics
pnpm --filter @object-ui/plugin-list lint0✖ 388 problems (0 errors, 388 warnings) — all pre-existing
pnpm exec vitest run packages/plugin-list/ (repo root)0Test Files 44 passed (44) · Tests 652 passed (652)
check-control-bytes0OK (scanned 4665 tracked text file(s); skipped 85 binary)
check-changeset-presence01 source file(s) of 1 released package(s) changed, and this change declares 1 changeset(s)
check-changeset-no-major0No changeset declares a major bump.
check-changeset-fixed0All workspace packages are in the changeset fixed group.
check-type-check-coverage045/46 via type-check, 0 known-broken, 1 not compiled
check-lint-coverage0lint coverage: 46/46 packages linted, 0 with outstanding errors
check-spec-symbol-derivation01290 files scanned against 4912 spec export names
check-i18n-call-site-keys0every in-scope call-site key resolves against the en pack
check-i18n-en-drift00 en value(s) changed — independent confirmation no string moved
check-action-forward-parity0action:menu owes 24, forwards 18
check-package-self-import0No package names itself inside its own src/.
check-phantom-dependencies0Every in-scope import is declared by the package that publishes it.

The last twelve were re-run on the final commit d05c0f2b8 with a clean tree, so the
three heavy gates above them ran on a byte-identical tree.

Narrowing, declared. The repo-wide pnpm lint and pnpm check were not run whole;
eslint . was run scoped to this package, which is the only package whose files changed.
That cannot hide a failure here: the diff adds zero non-comment lines, so no rule with a
code-shaped target can newly fire, and this repo does not enable type-aware linting
across package boundaries, so a comment in plugin-list cannot move any verdict on an
untouched file. performance-budget.yml triggers on packages/** and so will run — it
cannot move, because both emitted bundles are byte-identical by sha256.

Generated by Claude Code


Generated by Claude Code

…d docblock (#4559) (#4966)
Two comment-only corrections in packages/plugin-list/src/ListView.tsx.
objectui#4559 - the comment block above the sortFields memo still called a
formula field "the supported alternative ... which sorts like any text column".
The list.sortRelationalHint string in the same file has said the opposite since
objectui#4294, the memo below filters formula out via UNMATERIALIZED_FIELD_TYPES,
and the server answers such a sort with 400 INVALID_SORT (objectstack#6994). The
parenthetical now names the stored-field remedy the hint, the refusal and the
README share.
objectui#4966 - formatActionLabel's docblock had drifted two declarations up, so
the exported parseSortConfig carried two stacked leading comments and the helper
carried none. Because parseSortConfig is exported, vite-plugin-dts copied the
misattributed block into dist/ListView.d.ts, so the misattribution shipped to
consumers' editor hover and TypeDoc.
Prose only: zero non-comment lines change, the emitted bundle is byte-identical,
and the only dist delta is the .d.ts block removal above.
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 52 chunks)3785.7 KB3867.2 KB
Main entry chunk (gzip)151.5 KB350 KB
Entry fileindex-avY7FpUW.js
StatusPASS

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


📦 Bundle Size Report

PackageSizeGzipped
app-shell (index.js)10.04KB3.72KB
app-shell (runtime-config.js)8.91KB2.99KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)1.17KB0.53KB
auth (AuthProvider.js)29.34KB7.05KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)6.35KB2.43KB
auth (index.js)2.77KB1.22KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.02KB0.89KB
auth (useIsWorkspaceAdmin.js)3.04KB1.45KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)506.99KB113.73KB
core (index.js)4.51KB1.80KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)159.80KB44.33KB
fields (index.js)238.85KB60.13KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.22KB3.08KB
i18n (provider.js)23.13KB7.63KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)7.77KB3.13KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.35KB3.31KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.42KB1.42KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.81KB0.83KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.62KB12.83KB
plugin-charts (index.js)64.72KB18.35KB
plugin-chatbot (index.js)181.41KB43.22KB
plugin-dashboard (index.js)128.36KB32.95KB
plugin-designer (index.js)212.30KB42.80KB
plugin-detail (index.js)242.15KB60.89KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)125.07KB30.43KB
plugin-gantt (index.js)164.10KB39.87KB
plugin-grid (index.js)200.79KB54.26KB
plugin-kanban (index.js)52.93KB14.60KB
plugin-list (index.js)111.70KB27.17KB
plugin-map (index.js)20.06KB6.62KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.49KB11.93KB
plugin-timeline (index.js)26.68KB7.66KB
plugin-tree (index.js)8.50KB2.88KB
plugin-view (index.js)84.50KB20.68KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)3.77KB1.33KB
react (SchemaRenderer.js)43.66KB14.77KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)1.33KB0.69KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (index.js)4.77KB2.16KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)10.76KB3.17KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.29KB0.24KB
sdui-parser (validate.js)6.92KB2.40KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)0.20KB0.18KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)0.20KB0.18KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.87KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-retry.js)4.32KB2.02KB
types (index.js)3.08KB1.53KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)0.20KB0.18KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

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

@os-salesClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM review — ACCEPT (cards #4559 + #4966)

Gates. 22 named check runs read individually for completed + success at d05c0f2b8: 19 success, 3 skippedTest (coverage), the unexpanded Test (coverage shard …/4) matrix placeholder, and dependabot. All four real shards, Type Check, Lint, Build & E2E, Build Docs, Bundle Analysis, Live E2E, both doc type checks, Control Byte Scan, Skill Guide Path Check, Internal Docs Link Check and all three changeset checks green.

Both cards close here, as the triage note designated — one PR, two edits, itemized.

#4966 was not cosmetic, and the measurement is what showed that

The card and the triage grade both read this as a tidiness fix: a docblock two declarations away from the function it describes. Measured against the package's real build, it was reaching the publisheddist/ListView.d.ts on the exported parseSortConfig — so consumer editor hover and TypeDoc really were introducing the sort parser with a sentence about action labels. Moving it changes what ships.

That is what decided the changeset form: a real patch, not the empty frontmatter. dist/ListView.d.ts changes; dist/index.js and dist/index.umd.cjs are byte-identical by sha256, with grep -c 'Format an action identifier' returning 0 in both bundles.

My dispatch's mechanism was wrong, and you said so instead of working around it

I told you this repo builds packages with plain tsc under a deliberate removeComments: false, so a comment edit changes emitted dist/*.js bytes. That is false for this package.packages/plugin-list is built by vite build (rolldown bundle plus vite-plugin-dts); tsc appears only as --noEmit inside type-check, so removeComments never governs its emitted JS and the bundler strips these comments entirely.

Reporting that rather than quietly routing around it is the right call, and it is now a lane fact: the build system varies per package, so no repo-wide claim about emitted output holds. Four packages measured today, four different behaviours — react and core ship comments through plain tsc; types emits near-empty JS so only the .d.ts matters; plugin-list bundles and strips. The criterion I gave you — does consumer-visible API documentation change — is the one that survived all four, which is why it is the criterion.

#4559

The parenthetical now names the stored-field remedy. Both load-bearing claims were quoted as they read today rather than inherited: the hint string at :608-609 ("Not a formula field: it is virtual, so no column is stored for it and the server refuses to sort by one") and the filter at :2338. The membership chain was resolved to its source rather than stopped at the rename — UNMATERIALIZED_FIELD_TYPESSEARCH_VIRTUAL_TYPES from @objectstack/spec/datanew Set(['formula']), pinned by that package's own test. Formula is withheld and is refused, so the parenthetical was still wrong and there was nothing to stop for.

Anchoring by declaration rather than line number was necessary and done: the card's ~2152-2155 had drifted to 2309-2310, and its cited symbol had left the file entirely.

Evidence

Comment-only proved four ways rather than asserted: git diff -U0 with comment syntax filtered returns 0 lines; check-i18n-en-drift reports 0 en value(s) changed, so no string moved; both emitted bundles byte-identical by sha256; and each anchor asserted on disk by exact-string count before and after, never by an editor's exit code.

No test and no ablation — correctly stated as impossible here rather than manufactured.

On your open question: A, keep the dispatch form

No model identifier in any pushed artifact. That is this seat's standing rule and it is recorded on the seat post; the dispatch is the more specific instruction and it correctly won over the harness default. No change needed on future PRs.


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review August 21, 2026 18:16
@os-sales
os-sales added this pull request to the merge queueAug 21, 2026
Merged via the queue into main with commit d6613a2Aug 21, 2026
23 checks passed
@os-sales
os-sales deleted the claude/issue-4559-listview-comment-pair branch August 21, 2026 18:16
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment