Skip to content

feat(spec,rest): serve a per-column sortability projection with object metadata - #11214

Merged
os-sam merged 3 commits into
mainfrom
claude/issue-10235-sortability-signal
Aug 23, 2026
Merged

feat(spec,rest): serve a per-column sortability projection with object metadata#11214
os-sam merged 3 commits into
mainfrom
claude/issue-10235-sortability-signal

Conversation

@os-sam

Copy link
Copy Markdown
Collaborator

Fixes#10235

Spec/rest leg only — the consumer leg (grid drops the sort affordance where the signal says unsortable; the personalization PUT is never offered) is objectstack-ai/objectui#5729, which remains open and is Blocked-by this PR. objectstack-ai/objectui#5729 is not addressed here.

Maintainer ruling (2026-08-23, option A, recorded on the issue): the platform serves an explicit per-column sortability signal with object metadata; the grid reads the signal and never re-derives "virtual ⇒ unsortable" from field type.

Deliverable 1 — the closed "unsortable" category set, enumerated from the door predicates

Judged from what the two runtime doors (assertSortFieldsExist, metadata-protocol #6994; assertOrderByIsMaterializable, objectql #7095) and the lint predicate actually enforce:

#CategoryDoor behaviourEncoding in the signal
1Unknown namerefused 400 INVALID_SORTabsence from the projection map
2Dotted path (account.name)refused 400 INVALID_SORTabsence (entries are whole-column field names)
3Virtual type (SEARCH_VIRTUAL_TYPES = formula today)refused at both doorssortable: false, reason: 'virtual-type'
4Unprovisioned injected anchor on an ADR-0015 external object (#7865/#10474)accepted, silently dropped when the remote lacks the columnsortable: true, caveat: 'unprovisioned-anchor'

Considered and deliberately not members (rationale in the module docblock): summary/autonumber (sort correctly — measured #6924; virtuality is the storage predicate, never the write contract COMPUTED_VALUE_TYPES); encrypted/secret/json/vector etc. (stored columns, no door refuses them — marking them unsortable would invent an unenforced refusal, the mirror image of declared≠enforced); created_at/updated_at on a systemFields: false object (gate hard-admits them, no column provisioned — absent from the served field map, hence absent from the signal: the projection may be narrower than a gate that is known to degrade).

Deliverable 2 — declare + serve the projection

Contract (packages/spec/src/api/sortability.zod.ts, new):FieldSortabilitySchema / ObjectSortabilitySchema + the pure resolver resolveObjectSortability(doc). Derived from the spec's own predicates — isVirtualSearchField/SEARCH_VIRTUAL_TYPES (data/search-fields.ts, the same storage fact the two doors and validate-sortable-fields key on) and unprovisionedInjectedColumns (data/injected-system-column-provenance.ts, the #7865 derivation) — so there is no second hand-maintained flag anywhere. GetMetaItemResponseSchema gains an optional envelope-level sortability key.

Not an authorable key, by construction:FieldSchema is strictObject, so a key inside the served document is rejected by name and would otherwise become authorable — the exact rejected shape resolveInjectedColumnProvenance's docblock records. The projection rides the envelope beside item; the document is byte-untouched.

Serving door (mapped, as the dispatch required):GET /api/v1/meta/:type/:name in packages/rest/src/rest-server.ts. The splice is in translateMetaEnvelope — the one seam every single-item exit rebuilds its body through (#5563: cached, uncached, compound-name) — so the cached (default) branch serves the signal too, unlike the ADR-0010 lock keys (which come from the lock resolver the cached path never consults). Computed from the final post-ADR-0106-masking document, so the projection's domain equals the field set the caller is served. The view read (/meta/view/:name) deliberately carries nothing new: a grid resolves each column against its bound object's projection (objectui's grid already reads object schema through exactly this route — data-objectstack.getObjectSchemaGET /meta/object/:name), and one projection per object cannot drift across the N views over it. The /meta/:type LIST read is also unchanged: list items are bare documents (no envelope), and the strict-schema argument above applies to them verbatim.

Oracle (the #9313/#10234 harness cells)

The 3 measured refusal cells are pinned against the shipped corpus, in the apps that ship them, with the displayed-column premise asserted alongside so the pins cannot go vacuously green:

  • examples/app-crm/test/sortability-projection-oracle.test.tscrm_opportunity.expected_revenue (displayed in the aggregate's default list andlistViews.all) → sortable: false, reason: 'virtual-type'; anti-vacuity: amount/close_date/name stay sortable: true.
  • examples/app-showcase/test/sortability-projection-oracle.test.tsshowcase_project.budget_remaining → unsortable; budget/spent stay sortable.

Coupling proof (projection tracks the predicate): the serving layer calls the spec resolver (structural identity, not agreement-by-coincidence); the rest test additionally asserts wire-projection ≡ resolveObjectSortability(served item) on both branches; and the spec test loops the unsortable pin over SEARCH_VIRTUAL_TYPES itself, so widening the predicate widens the projection with no edit to the pin.

Tests

  • packages/spec/src/api/sortability.test.ts — resolver pins: predicate-coupled loop, summary/autonumber anti-overreach, anchor caveat (external vs author-declared vs local), id append, both field-map shapes, wire validity.
  • packages/rest/src/meta-object-sortability-projection.test.ts — envelope carries the projection on the cached (default) and uncached branches; plural-URL fold; a non-object read carries nosortability key; agreement with the spec resolver.
  • The two oracle files above.

File surface

packages/spec/src/api/{sortability.zod.ts,sortability.test.ts,index.ts,protocol.zod.ts}, packages/spec/src/type-alias-convention.pin.test.ts (the two new aliases join the ADR-0122 isomorphic pin registry — no defaults/transforms by design), packages/rest/src/{rest-server.ts,meta-object-sortability-projection.test.ts}, the two example oracle tests, one changeset, one hand-written docs count line (content/docs/getting-started/quick-reference.mdx, 28→29 — the gate-named consequence of the new reference page), and the regenerated spec artifacts (api-surface/, export-origins/, authorable-surface/, json-schema.manifest/, content/docs/references/**, strictness-ledger counts). Fenced files (packages/spec/src/data/field.zod.ts, packages/spec/src/data/object.zod.ts) untouched; packages/spec/src/ui/view.zod.ts untouched.

Verification

Per-gate verdict lines land in the os-dev-report comment on #10235 (gate list derived by node scripts/pm/dispatch-gates.mjs at the final head; CI convergence review is the PM's, per the standing dispatch contract).

Generated by Claude Code


Generated by Claude Code

…t metadata
Fixes#10235 (spec/rest leg; consumer leg is objectui#5729).
2026-08-23 ruling, option A: the platform serves an explicit per-column
sortability signal with object metadata; the grid reads the signal and never
re-derives 'virtual => unsortable' from field type.
- packages/spec/src/api/sortability.zod.ts: ObjectSortabilitySchema +
resolveObjectSortability, derived from the spec's own storage predicates
(isVirtualSearchField / SEARCH_VIRTUAL_TYPES, unprovisionedInjectedColumns)
so the signal cannot drift from what the runtime doors (#6994/#7095) refuse.
The closed category set is enumerated in the module docblock.
- GetMetaItemResponseSchema gains an optional envelope-level 'sortability'
key (never inside the document: FieldSchema is strict and the key must stay
un-authorable).
- rest: computed in translateMetaEnvelope - the one seam every single-item
read exit passes through - so the cached (default) branch serves it too,
from the final post-masking document.
- Tests: spec resolver pins (predicate-coupled via SEARCH_VIRTUAL_TYPES loop,
summary/autonumber anti-overreach, anchor caveat), rest branch/agreement
pins, and the shipped-corpus oracle pins in app-crm / app-showcase
(crm_opportunity.expected_revenue x2 views, showcase_project.budget_remaining).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RadETjNRLALFLhFA3xehZP
…r the sortability projection
- FieldSortability / ObjectSortability flip to z.input (bare = author state)
and join the isomorphic pin registry (no defaults/transforms by design).
- quick-reference API Protocol page total 28 -> 29 (the new reference page).
- rest test double now folds the plural URL spelling like the real producer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RadETjNRLALFLhFA3xehZP
@github-actions

github-actionsBot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/rest, @objectstack/spec, touching 17 documentable anchor(s).

19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json d25f700735373f65ec86bc009fc87034ff6efa54.

3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/spec/api-surface/api.json, packages/spec/authorable-surface/api.json, packages/spec/export-origins/api.json, …) — pages documenting those are invisible to this run
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 128 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json d25f700735373f65ec86bc009fc87034ff6efa54packageMentionDocs.

Which tree this was computed on

This run read content/docs from 362f1b25927ef6374286ee344ff449944a6808b0 — the merge of head f87dbf113156220004ef3aaa1253a5a214f60fc8 into base d25f700735373f65ec86bc009fc87034ff6efa54, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 362f1b25927ef6374286ee344ff449944a6808b0 && git checkout 362f1b25927ef6374286ee344ff449944a6808b0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d25f700735373f65ec86bc009fc87034ff6efa54 f87dbf113156220004ef3aaa1253a5a214f60fc8 && git checkout -B drift-repro d25f700735373f65ec86bc009fc87034ff6efa54 && git merge --no-ff f87dbf113156220004ef3aaa1253a5a214f60fc8
node scripts/docs-audit/affected-docs.mjs --json d25f700735373f65ec86bc009fc87034ff6efa54

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs d25f700735373f65ec86bc009fc87034ff6efa54 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…c errors
The @objectstack/rest test layer is gated by a shrink-only TEST_DEBT ledger
(155 raw tsc errors); the new test file added 2 (TS2835 missing .js extension,
TS2550 Array.prototype.at under the repo lib). Explicit extension + indexed
access; re-measured 157 -> 155 with the ledger's own synthetic project.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RadETjNRLALFLhFA3xehZP
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sam@claude