Uh oh!
There was an error while loading. Please reload this page.
feat(core,data-objectstack,plugin-grid): consume the platform's per-column sortability signal - #6109
Merged
Merged
Conversation
…n sortability signal Downstream leg of objectstack#10235's ruling A. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
…tating them check:spec-symbols caught four hand-written declarations under names @objectstack/spec/api already owns. Re-exported, and the changeset added. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CSoz9uGhaaSgiq3hshtN7L
Contributor
✅ Console Performance Budget
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
Size Limits
|
This was referenced Aug 24, 2026
yinlianghui
marked this pull request as ready for review
August 24, 2026 17:53
This was referenced Aug 24, 2026
Uh oh!
There was an error while loading. Please reload this page.
This was referenced Aug 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#5729
Downstream leg of objectstack-ai/objectstack#10235 (maintainer 2026-08-23, ruling A): the platform serves an explicit per-column sortability signal, and the grid reads it rather than re-deriving "virtual means unsortable" from field type.
All gate results below are from
57870ce55, the branch head.The measured envelope shape, versus the one the thread described
Read from the merged upstream change in the
objectstackcheckout, not from the issue thread. The description was accurate in every particular.packages/spec/src/api/protocol.zod.ts:413—GetMetaItemResponseSchemagainssortability: ObjectSortabilitySchema.optional(), on the envelope besideitem, never inside it (FieldSchemais astrictObject, so the key has to stay un-authorable).packages/spec/src/api/sortability.zod.ts—{ fields: Record<string, { sortable: boolean; reason?: 'virtual-type'; caveat?: 'unprovisioned-anchor' }> }, exactly as claimed.packages/rest/src/rest-server.ts:2609— computed intranslateMetaEnvelope, the one seam every single-item exit passes through, so the signal is present on every branch including the cached one.Two details worth carrying forward that the thread did not spell out: the projection's domain always includes
ideven when the document declares no such field, and it is computed from the FINAL post-masking document, so its domain equals the field set this caller is served.Verified by running the platform's own
resolveObjectSortabilityfrom the installed@objectstack/spec@17.2.0against the oracle documents.crm_opportunity.expected_revenueandshowcase_project.budget_remainingboth come back{ sortable: false, reason: 'virtual-type' };amount,name,budgetandidcome back{ sortable: true }.The real work: the signal was reaching the browser and being thrown away
ObjectStackAdapter.fetchObjectSchemaFreshunwrapped the response todata.itemand returned it. The envelope key — and with it the entire signal — was discarded one line before its only consumer, so every UI reader saw a document with nothing on it and could only conclude the platform had sent nothing. That line is the substance of this change.It now survives the unwrap, carried on the schema under
Symbol.for('objectui.objectSortability'). The symbol is load-bearing, not decoration:JSON.stringify,Object.keysand spread all skip it, so a schema handed back at a metadata write endpoint cannot take the projection into a body the server parses strictly. Putting it on a string key would have undone the upstream decision to keep it offitem, one repo away. Pinned directly, with a counter-probe proving the assertions measure invisibility rather than a failed attach.The contract, and the asymmetry it turns on
@object-ui/coregains the one spelling:isPlatformSortableField(projection, name)istrueiff an entry EXISTS for the name and sayssortable: true.Absence is a refusal. It is how the platform encodes an unknown name, a dotted path and an unprovisioned audit column, all three of which the runtime doors reject — so the
!== falsespelling every other optional flag in this repo uses gets exactly that family backwards. Ablation A below is the pin.A projection that is absent ALTOGETHER is a different question with a different answer.
readObjectSortabilityreturnsundefinedwhen the metadata response carried nosortabilitykey, which means "no signal was served", not "nothing is sortable"; the two are typed apart so a caller cannot conflate them.isPlatformSortableFieldtakes a non-optional projection for the same reason.The spec's own names are re-exported rather than restated.
check:spec-symbolscaught four hand-written declarations under names@objectstack/spec/apialready owns (FIELD_UNSORTABLE_VIRTUAL_TYPE,FIELD_SORTABLE_UNPROVISIONED_ANCHOR,FieldSortability,ObjectSortability); the second commit replaces all four with re-exports, which is the contract-first shape and one fewer thing to drift.What changed in the grid
The header click on a refused column ceases to exist, so neither the old silent-unordered result nor the
400 INVALID_SORTthat replaced it is reachable from it.The restore leg gets its own guard.
DataTableemitsonSortChangeonly out ofhandleSort, itself gated oncol.sortable !== false, so withholding the affordance already stops a NEW refused sort from being created. It says nothing about one ALREADY in stored view state, persisted before the signal existed: replayed, it would paint an active-sort arrow on a header that offers no click and ride into the nextpersistViewPatch({ sort })the toolbar issues for an unrelated reason. Both directions now run through the same filter — what the grid renders the sort state as, and what it emits back to whoever persists it.The relational carve-out is untouched, and deliberately not delegated to this signal. Measured and pinned: the projection answers
sortable: truefor alookup, because the platform's question is whether it can order by the stored foreign key, which it can. The grid's question is whether that order means anything beside a column of names, and it does not. Folding the two together would have handed every relational header its sort click back.Columns carrying
caveat: 'unprovisioned-anchor'keep their click — default A, held rather than decided. See the ruling request below.Verification
Directions predicted before running; both ablations restore under
trap ... EXIT INT TERMwith cwd-independent commands, andgit diff HEAD --statwas confirmed empty after each. No rebuild is needed for either: the rootvitest.config.mtsaliases every@object-ui/*specifier to that package'ssrc/, so the mutated source is what the run executes.Ablation A —
isPlatformSortableFieldrewritten to the naive!== falsespelling. Predicted: red on the ABSENT-entry cells ONLY, with thesortable: falsecells staying green, since that is the whole asymmetry. Observed exactly that: 4 failed, 15 passed; the four are "refuses an ABSENT entry", "refuses an empty / malformed field name and a malformed entry", "drops a persisted sort on a column the platform refuses", and the grid's "withholds a column ABSENT from the projection".expected_revenueandbudget_remainingstayed green throughout, confirming the two rules are separately load-bearing. Mutation confirmed on disk by grepping the injected marker (1) and the removed anchor (0) separately, with the anchor asserted unique first.Ablation B — the transit carry dropped (
return item). Predicted: the three carry cells red, the two "no projection served" cells green. Observed exactly that: 3 failed, 2 passed.Cross-package type reverse-check — a value the new signature must reject, fed to
isPlatformSortableFieldfromplugin-grid. Red as predicted, witherror TS2345naming the spec's literal union ("virtual-type" | undefined,"unprovisioned-anchor" | undefined), which proves tsc read the freshly built.d.tsacross the package boundary rather than a cached one.One red found and fixed rather than reported: my first fixture returned the same object literal from every mocked response, and the adapter stamps the projection onto the document it is handed — so a previous test's stamp leaked into the "no signal served" cells and made them pass while measuring nothing. The mock now returns fresh JSON per response, as a real
res.json()does.Gates (each exit code captured before any pipe; each verdict quoted from the gate's own output):
pnpm --filter @object-ui/core type-checkpnpm --filter @object-ui/data-objectstack type-checkpnpm --filter @object-ui/plugin-grid type-checknpx vitest run packages/core/src packages/data-objectstack/srcTest Files 143 passed (143)/Tests 2628 passed (2628)npx vitest run packages/plugin-grid/srcTest Files 86 passed (86)/Tests 811 passed (811)pnpm --filter '...@object-ui/core' type-check(downstream)npx eslint <merge-base delta>377 problems (0 errors, 377 warnings)— all pre-existingno-explicit-anypnpm check:spec-symbolspnpm check:esm-specifiersno un-ledgered package emits an extensionless relative specifierpnpm check:phantom-depsEvery in-scope import is declared by the package that publishes itpnpm check:self-importNo package names itself inside its own src/node scripts/check-changeset-presence.mjs7 source file(s) of 3 released package(s) changed, and this change declares 1 changeset(s)node scripts/check-changeset-fixed.mjsAll workspace packages are in the changeset fixed groupnode scripts/check-changeset-no-major.mjsNo changeset declares a major bumpnode scripts/check-control-bytes.mjsscanned 5040 tracked text file(s)The downstream sweep's direction, stated because the claim is unreadable without it:
'...@object-ui/core'is the PREFIX form, so it selects the packages that DEPEND ON core — the consumers a contract change lands on. 35 ran the script; exactly one failed,apps/site, onCannot find module '@object-ui/example-schema-catalog'. That is the unbuilt-closure class the sweep is known to produce for workspaces outsidepackages/**(I built--filter './packages/**'first, which is whypackages/componentsandplugin-dashboardpass here and did not on the first attempt). Noerror TSanywhere else, and nothing naming sortability.Gate set derived by enumerating each CI job's own step list rather than top-level script names, per the standing rule — which is how
check:esm-specifiersandcheck:spec-symbolsgot run at all; both live as steps inside theType Checkjob under names that do not read like one, andcheck:spec-symbolswas a genuine red.Ruling requested, not taken
caveat: 'unprovisioned-anchor'stays on default A — the click is kept. The runtime accepts these sorts; refusing what the platform does not refuse would recreate declared-versus-enforced drift in mirror image. Option B (drop the affordance) or C (click plus a degraded-order warning) remain open and are the maintainer's to pick; the caveat is preserved through the whole path and pinned, so either is a small change on top.The one thing I would like ruled, because it is the closest this change comes to scope item 3's prohibition: when NO projection was served,
withSortabilitystill falls back to the pre-existingisUnmaterializedFieldTyperead. My reasoning for keeping it, and why I think it is not the shadow derivation the ruling forbids: it is bound to@objectstack/spec's ownSEARCH_VIRTUAL_TYPES, the identical predicate the platform computes its projection from, so it cannot disagree aboutformula; it is unreachable the moment a backend serves the signal; and deleting it outright would hand every formula header its refused click back on any deployment older than objectstack#10235. It is a compatibility floor, not a second judge, and it is commented as one — but if the ruling wants it gone now, it is a two-line deletion plus one test.Runtime dependency and what was actually runnable
The installed
@objectstack/spec@17.2.0carries the change, and the pins here run against that resolver directly. I did not boot a dev stack, so the three oracle cells are pinned at unit level against the platform's own resolver rather than claimed as a served-backend measurement — which is the discipline the requeue obligation asked for when the runtime leg cannot be run, and I would rather report it that way than report an oracle I did not execute. Every negative cell carries its positive control in the same render (a sortable sibling column, still clickable, still putting its$orderbyon the wire), so "the click is gone" cannot be satisfied by a grid that rendered nothing.Scope
packages/componentswas not needed —DataTablealready renders no cursor, no click handler and no sort icon forcol.sortable === false, and emitsonSortChangeonly from the gatedhandleSort.plugin-detailuntouched.Filed out of scope, unassigned: #6108 — the other two sort-axis consumers (
ListView's toolbar sort picker andRelatedList) still re-derive from field type. The picker one is not cosmetic: its in-use exception keeps a refused field listed, so an unrelated edit in that popover re-emits the whole sort array intopersistViewPatch({ sort }). This PR closes that leak at the grid seam; the picker is a second door onto the same stored state.Generated by Claude Code