docs(spec): cross-reference the two $orderby declarations and pin their disjoint accept sets - #19018
Conversation
… pin Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
…heir disjoint accept sets `ODataQuerySchema.$orderby` (`string | string[]`) and `QueryTransportParamsSchema.$orderby` (`DataEngineSortSchema`: the record maps and `SortNode[]`) are complementary refusals — each accepts exactly what the other rejects — and neither pointed at the other, so reading one of them carefully and completely still produced the wrong answer about the other. No accept set moves. Both docblocks now name the other declaration, say which one grades a query bag (`FindDataRequestSchema.query` via `QueryWithTransportSchema`), and say what actually parses the string forms (`normalizeSortNodes` at the metadata-protocol ingress, not a schema). `src/api/odata-orderby-dual-declaration.test.ts` holds both accept sets and their disjointness, so widening or narrowing either side turns red. Claude-Session: https://claude.ai/code/session_019srGWGCBBCBHqcDoRZpQRh Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 32486df103f951ec13adef9a783dd024e8805e8a && git checkout 32486df103f951ec13adef9a783dd024e8805e8a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b84a84745aeba97a240681d7742ac9abe5f93719 c7e22addb4cf9435bc3cb401e2a3545f3fce986d && git checkout -B drift-repro b84a84745aeba97a240681d7742ac9abe5f93719 && git merge --no-ff c7e22addb4cf9435bc3cb401e2a3545f3fce986d
node scripts/docs-audit/affected-docs.mjs --json b84a84745aeba97a240681d7742ac9abe5f93719
|
…ed on the origin/main merge The merge of origin/main (b146102) routed four paths to the os-regen driver, which defers rather than text-merging: the three api-surface-declarations shards automation.txt, data.txt and ui.txt, and the generated reference page content/docs/references/ui/component.mdx. On every one of them the driver kept this branch's side and silently dropped main's, so step 2 of the sanctioned sequence restored main's side into the worktree and this commit re-derives all four from the merged tree. What each side contributed, now both present: ui.txt / component.mdx main's element:text.variant widening to the published nine (#19019), plus this branch's evaluated-slot narrowing data.txt main's transport no-transactions declaration (#18890) and the $orderby dual declaration (#19018), plus this branch's `source: string` narrowing on ConditionalValidationSchema and CrossFieldValidationSchema automation.txt main's structured-region pause/end refusal (#18688), plus this branch's narrowing The two MIXED, deliberately unrouted paths were hand-resolved by git's ordinary three-way merge and verified rather than eyeballed: registry.ts's hand-written remainder (generated regions stripped with the merge script's own awk) is byte-identical across base, both sides and the merge, and its line count is exactly additive (17142 + 121 + 74 = 17337), with both sides' migration entries present by id. component.zod.ts is additive too (3750 + 4 + 45 = 3799) and its single .superRefine() is untouched. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <noreply@anthropic.com>
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:generatedreports all 16 generated artifacts up to date,check:api-surface,check:api-surface-declarations,check:authorable-surfaceandcheck:docsincluded.The card
$orderbyis declared twice inpackages/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) withsafeParseagainst a fresh build of both schemas:$orderbyvalueODataQuerySchema(src/api/odata.zod.ts)QueryTransportParamsSchema=DataEngineSortSchema(src/data/data-engine.zod.ts)'name desc'/'-created_at'['name desc', 'email asc'][{field, order}]asc/descrecord map1/-1record mapThe 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 thebuildUrlhelper in its own file.Instrument:
git grep -n ODataQuerySchemaandgit grep -n '\bODataQuery\b'over this worktree at43f476688. Unit: files naming the symbol. Result: the declaration is consumed byOData.buildUrlat the foot of its own file, by its own unit testsrc/api/odata.test.ts, and bysrc/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
FindDataRequestSchemalands onpackages/rest/src/rest-server.ts:9019, thePOST /data/:object/queryhandler thatsafeParses its body against it; overQueryTransportParamsSchemait lands onpackages/rest/src/rest-server-canonical-query-ast.test.ts. So the instrument does find consumers outsidepackages/specwhen there are any — the zero is a reading, not a dead instrument.⇒
ODataQuerySchemagrades no runtime door. The declaration that grades a query bag isQueryTransportParamsSchema, reached fromFindDataRequestSchema.querythroughQueryWithTransportSchema.Context the card predates: #18704 already settled which spelling is canonical
0b788da89declared 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 answer400at 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 — wideningDataEngineSortSchemato 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 behindGET /data/:object, the export route and in-processfindData, and it reads'name desc','-created_at'and thestring[]form. Measured at the exact input shaperest-server.tsbuilds:POST /data/:object/querybodyFindDataRequestSchema.safeParse{"$orderby": "name desc"}400 VALIDATION_FAILEDatquery.$orderby{"$orderby": ["name desc"]}400 VALIDATION_FAILEDatquery.$orderby{"$orderby": {"created_at": "desc"}}orderBy: [{field, order}]{"sort": "-created_at"}400 VALIDATION_FAILEDatquery.sortThe 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:
src/api/odata.zod.ts— the docblock aboveODataQuerySchemanow says it grades no runtime door, namesQueryTransportParamsSchemaas 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$orderbymember carries the same pointer at the point of use.src/data/data-engine.zod.ts— the reciprocal pointer, inside the paragraph that states the refusal. It namesODataQuerySchema.$orderbyas the second declaration, records that it grades nothing, and records the cost already paid: objectui#9554 was filed, triaged, graded and dispatched against a shippedobject-gridproducer that had been sending the canonical shape all along.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 twoFindDataRequestSchema.queryis graded by.⛔ No
.describe(), no Zod type, no export and no authorable key is touched. EverysafeParseverdict 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(anchor1 -> 0, blobb25169449f69 -> 25040216bf74), restored under atrap. No dist preflight was owed: the pin imports./odata.zodrelatively, so it resolves to source and no build stands between the mutation and the verdict.asc/descrecord arm toODataQuerySchema.$orderby, so the two accept sets overlap on one value.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 []) andevery declared $orderby value parses under exactly one of them(expected [ 1, 1, 1, 1, 2, 1, 1 ]).blob after restore b25169449f69 == blob at HEAD b25169449f69,git diff HEADempty.Evidence, at
c7e22addbpnpm --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 undertsconfig.test.jsonandtest-typecheck-debt.jsonis unmoved at 54 files / 259 errors / 144 pinned signatures.pnpm --filter @objectstack/spec test(projectlocal) — 490 files / 14234 tests passed, exit 0.scripts/pm/os-verify-lock.sh; the verdicts above are itsVERDICT command-exitlines, 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.
content/docs/api/data-api.mdxteaches twoPOST /data/:object/querysort 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 shaperest-server.tsbuilds: the first is 200;{"orderBy": ["-created_at"]}is400 VALIDATION_FAILEDatquery.orderBy.0(expected object, received string) and{"orderBy": {"created_at": "desc"}}is400 VALIDATION_FAILEDatquery.orderBy(expected array, received object). CanonicalorderByisz.array(SortNodeSchema); the record map and the shorthand array are transport-slot values, so they have to arrive on$orderby/sort.@example Programmatic Useinsrc/api/odata.zod.ts's file-level docblock parses to{}. It writesselect/filter/orderby/top/skip/expand/count— unprefixed — against the typeODataQuery, whose every key carries a$.ODataQuerySchema.safeParseon that bag verbatim succeeds and returns{}: every key is stripped. The block ships tocontent/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 fencedcontent/docs/references/**for the round.Generated by Claude Code