Commit dbd4744
docs(spec): cross-reference the two
Fixes #18977
Clause-②: no — no accept set moves, and no export is added, removed or
renamed. The diff is two docblocks in published source, one new pin
test, and the changeset. Measured, not asserted: `check:generated`
reports all 16 generated artifacts up to date, `check:api-surface`,
`check:api-surface-declarations`, `check:authorable-surface` and
`check:docs` included.
## The card
`$orderby` is declared twice in `packages/spec`, and the two
declarations are **complementary refusals** — each accepts exactly what
the other rejects — with no cross-reference in either direction.
Re-measured on this branch's base (`43f4766889`) with `safeParse`
against a fresh build of both schemas:
| `$orderby` value | `ODataQuerySchema` (`src/api/odata.zod.ts`) |
`QueryTransportParamsSchema` = `DataEngineSortSchema`
(`src/data/data-engine.zod.ts`) |
|:---|:---|:---|
| `'name desc'` / `'-created_at'` | accepted | REFUSED |
| `['name desc', 'email asc']` | accepted | REFUSED |
| `[{field, order}]` | REFUSED | accepted |
| the `asc`/`desc` record map | REFUSED | accepted |
| the `1`/`-1` record map | REFUSED | accepted |
The premise holds exactly as filed.
## The reading the card flagged as not re-derived, re-derived here
The card said — explicitly as the filer's reading — that
`ODataQuerySchema`'s only in-repo consumer is the `buildUrl` helper in
its own file.
**Instrument**: `git grep -n ODataQuerySchema` and `git grep -n
'\bODataQuery\b'` over this worktree at `43f476688`. **Unit**: files
naming the symbol. **Result**: the declaration is consumed by
`OData.buildUrl` at the foot of its own file, by its own unit test
`src/api/odata.test.ts`, and by `src/type-alias-convention.pin.test.ts`
(a generic pin that names every schema in the module). Everything else
is a generated artefact — `api-surface*`, `authorable-surface*`,
`declaration-map`, `export-origins`, `json-schema.manifest` — or the
generated reference page. **Zero** routes, ingress paths or normalizers.
**Lit control, same instrument, same tree**: the same grep over
`FindDataRequestSchema` lands on
`packages/rest/src/rest-server.ts:9019`, the `POST /data/:object/query`
handler that `safeParse`s its body against it; over
`QueryTransportParamsSchema` it lands on
`packages/rest/src/rest-server-canonical-query-ast.test.ts`. So the
instrument does find consumers outside `packages/spec` when there are
any — the zero is a reading, not a dead instrument.
⇒ **`ODataQuerySchema` grades no runtime door.** The declaration that
grades a query bag is `QueryTransportParamsSchema`, reached from
`FindDataRequestSchema.query` through `QueryWithTransportSchema`.
## Context the card predates: #18704 already settled which spelling is
canonical
`0b788da89` declared the query transport dialect as the flattened
spelling of the QueryAST, and its own body names the OData sort
expression among the shapes that now answer `400` at the ingress. The
reason is in the source, verbatim: 「⛔ Three shapes are deliberately NOT
declared, because lowering them means PARSING — and a second parser
beside the door's is how one rule gets two implementations that
disagree」. So option C on the card — widening `DataEngineSortSchema` to
accept the string forms — is the thing that commit refused, and option B
— widening the OData schema — moves a published accept set. Both are
maintainer questions, not this PR.
**And the string forms are not unserved**, which is the part neither
declaration says. `normalizeSortNodes`
(`packages/metadata-protocol/src/protocol.ts`) is the one shared ingress
normalizer behind `GET /data/:object`, the export route and in-process
`findData`, and it reads `'name desc'`, `'-created_at'` and the
`string[]` form. Measured at the exact input shape `rest-server.ts`
builds:
| `POST /data/:object/query` body | `FindDataRequestSchema.safeParse` |
|:---|:---|
| `{"$orderby": "name desc"}` | `400 VALIDATION_FAILED` at
`query.$orderby` |
| `{"$orderby": ["name desc"]}` | `400 VALIDATION_FAILED` at
`query.$orderby` |
| `{"$orderby": {"created_at": "desc"}}` | 200, folds to `orderBy:
[{field, order}]` |
| `{"sort": "-created_at"}` | `400 VALIDATION_FAILED` at `query.sort` |
The same querystring on the GET route works. The difference is the
**door**, and neither door is `ODataQuerySchema`.
## What this PR changes
Option A on the card, and nothing else — the reader's half of the
defect:
1. **`src/api/odata.zod.ts`** — the docblock above `ODataQuerySchema`
now says it grades no runtime door, names `QueryTransportParamsSchema`
as the declaration that does, carries the complementary-refusal table,
says why the gap is a decision rather than a defect, and says what
actually parses the string forms. The `$orderby` member carries the same
pointer at the point of use.
2. **`src/data/data-engine.zod.ts`** — the reciprocal pointer, inside
the paragraph that states the refusal. It names
`ODataQuerySchema.$orderby` as the second declaration, records that it
grades nothing, and records the cost already paid: objectui#9554 was
filed, triaged, graded and dispatched against a shipped `object-grid`
producer that had been sending the canonical shape all along.
3. **`src/api/odata-orderby-dual-declaration.test.ts`** — 25 cases, the
mechanical half of the cross-reference: each side's accept set, their
disjointness (with the lit control that neither set is empty, since two
schemas that accept nothing are also disjoint), and which of the two
`FindDataRequestSchema.query` is graded by.
⛔ No `.describe()`, no Zod type, no export and no authorable key is
touched. Every `safeParse` verdict on both declarations is the same
before and after.
## Reverse verification — the pin is capable of failing
One-off, committed first, mutated on disk through
`scripts/ablation-replace.mjs` (anchor `1 -> 0`, blob `b25169449f69 ->
25040216bf74`), restored under a `trap`. No dist preflight was owed: the
pin imports `./odata.zod` relatively, so it resolves to source and no
build stands between the mutation and the verdict.
- **Mutation**: add the `asc`/`desc` record arm to
`ODataQuerySchema.$orderby`, so the two accept sets overlap on one
value.
- **Predicted direction**: turn red — the OData side's refusal case and
both disjointness cases.
- **Observed**: `3 failed | 22 passed (25)` — `refuses the asc/desc
record map`, `no declared $orderby value parses under both` (`expected [
'the asc/desc record map' ] to deeply equal []`) and `every declared
$orderby value parses under exactly one of them` (`expected [ 1, 1, 1,
1, 2, 1, 1 ]`).
- **Restore proven by state, not by exit code**: `blob after restore
b251694 == blob at HEAD b251694`, `git diff HEAD` empty.
## Evidence, at `c7e22addb`
- `pnpm --filter @objectstack/spec build` — exit 0, 34/34 declaration
files emitted.
- `pnpm --filter @objectstack/spec check:generated` — exit 0, **all 16
generated artifacts up to date**.
- `pnpm --filter @objectstack/spec typecheck` — exit 0; the test layer
compiles under `tsconfig.test.json` and `test-typecheck-debt.json` is
unmoved at 54 files / 259 errors / 144 pinned signatures.
- `pnpm --filter @objectstack/spec test` (project `local`) — **490 files
/ 14234 tests passed**, exit 0.
- New pin alone: 25 passed.
- Every heavy run went through `scripts/pm/os-verify-lock.sh`; the
verdicts above are its `VERDICT command-exit` lines, not bare `$?`.
## Acceptance notes
Two findings outside this card's scope. ⛔ Not filed by me and ⛔ not
repaired here; they are in the report for the dispatching seat.
1. **`content/docs/api/data-api.mdx` teaches two `POST
/data/:object/query` sort spellings that the route refuses.** It says
sorts accept `{"orderBy": [{"field": "created_at", "order": "desc"}]}`,
`{"orderBy": ["-created_at"]}` or `{"orderBy": {"created_at": "desc"}}`,
"all equivalent". Measured at the shape `rest-server.ts` builds: the
first is 200; `{"orderBy": ["-created_at"]}` is `400 VALIDATION_FAILED`
at `query.orderBy.0` (`expected object, received string`) and
`{"orderBy": {"created_at": "desc"}}` is `400 VALIDATION_FAILED` at
`query.orderBy` (`expected array, received object`). Canonical `orderBy`
is `z.array(SortNodeSchema)`; the record map and the shorthand array are
transport-slot values, so they have to arrive on `$orderby` / `sort`.
2. **The `@example Programmatic Use` in `src/api/odata.zod.ts`'s
file-level docblock parses to `{}`.** It writes `select` / `filter` /
`orderby` / `top` / `skip` / `expand` / `count` — unprefixed — against
the type `ODataQuery`, whose every key carries a `$`.
`ODataQuerySchema.safeParse` on that bag verbatim succeeds and returns
`{}`: every key is stripped. The block ships to
`content/docs/references/api/odata.mdx`, so it is a published example.
Left alone here on purpose: the file-level docblock is the one part of
this file that feeds the generated reference page, and this lane fenced
`content/docs/references/**` for the round.
---
_Generated by [Claude
Code](https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh)_
---------
Co-authored-by: Claude <noreply@anthropic.com>$orderby declarations and pin their disjoint accept sets (#19018)1 parent 2d235bc commit dbd4744
4 files changed
Lines changed: 241 additions & 0 deletions
File tree
- .changeset
- packages/spec/src
- api
- data
Lines changed: 23 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
Lines changed: 149 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
71 | 71 | | |
72 | 72 | | |
73 | 73 | | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
74 | 115 | | |
75 | 116 | | |
76 | 117 | | |
| |||
115 | 156 | | |
116 | 157 | | |
117 | 158 | | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
118 | 168 | | |
119 | 169 | | |
120 | 170 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
830 | 830 | | |
831 | 831 | | |
832 | 832 | | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
| 846 | + | |
| 847 | + | |
| 848 | + | |
| 849 | + | |
| 850 | + | |
| 851 | + | |
833 | 852 | | |
834 | 853 | | |
835 | 854 | | |
| |||
0 commit comments