Skip to content

docs(spec): cross-reference the two $orderby declarations and pin their disjoint accept sets - #19018

Merged
os-elon-musk merged 2 commits into
mainfrom
claude/issue-18977-orderby-declared-twice
Sep 18, 2026
Merged

os-elon-musk merged 2 commits into
mainfrom
claude/issue-18977-orderby-declared-twice

Conversation

@os-elon-musk

Copy link
Copy Markdown
Collaborator

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 safeParses 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 b25169449f69 == blob at HEAD b25169449f69, 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

…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>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Sep 18, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 5 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/spec/src/data/data-engine.zod.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

10 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via data.create (sdk, the route ledger binds it to POST /api/v1/data/:object, selected by route anchor /data/:object; the route ledger binds it to POST /data/:object), data.find (sdk, the route ledger binds it to GET /api/v1/data/:object, selected by route anchor /data/:object; the route ledger binds it to GET /data/:object), data.query (sdk, the route ledger binds it to POST /api/v1/data/:object/query, selected by route anchor /data/:object/query; the route ledger binds it to POST /data/:object/query))
  • content/docs/api/data-api.mdx (via /data/:object/query (route, a path literal in a comment in ODataQuerySchema; a path literal in a comment on a changed line))
  • content/docs/api/data-flow.mdx (via data.create (sdk, the route ledger binds it to POST /api/v1/data/:object, selected by route anchor /data/:object; the route ledger binds it to POST /data/:object))
  • content/docs/api/environment-routing.mdx (via data.find (sdk, the route ledger binds it to GET /api/v1/data/:object, selected by route anchor /data/:object; the route ledger binds it to GET /data/:object))
  • content/docs/api/error-catalog.mdx (via data.create (sdk, the route ledger binds it to POST /api/v1/data/:object, selected by route anchor /data/:object; the route ledger binds it to POST /data/:object))
  • content/docs/api/wire-format.mdx (via /data/:object/query (route, a path literal in a comment in ODataQuerySchema; a path literal in a comment on a changed line))
  • content/docs/data-modeling/queries.mdx (via /data/:object/query (route, a path literal in a comment in ODataQuerySchema; a path literal in a comment on a changed line))
  • content/docs/deployment/troubleshooting.mdx (via data.find (sdk, the route ledger binds it to GET /api/v1/data/:object, selected by route anchor /data/:object; the route ledger binds it to GET /data/:object))
  • content/docs/kernel/runtime-services/data-service.mdx (via data.create (sdk, the route ledger binds it to POST /api/v1/data/:object, selected by route anchor /data/:object; the route ledger binds it to POST /data/:object), data.find (sdk, the route ledger binds it to GET /api/v1/data/:object, selected by route anchor /data/:object; the route ledger binds it to GET /data/:object), data.query (sdk, the route ledger binds it to POST /api/v1/data/:object/query, selected by route anchor /data/:object/query; the route ledger binds it to POST /data/:object/query), /data/:object/query (route, a path literal in a comment in ODataQuerySchema; a path literal in a comment on a changed line))
  • content/docs/permissions/authentication.mdx (via data.find (sdk, the route ledger binds it to GET /api/v1/data/:object, selected by route anchor /data/:object; the route ledger binds it to GET /data/:object))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v16.mdx (via data.create (sdk, the route ledger binds it to POST /api/v1/data/:object, selected by route anchor /data/:object; the route ledger binds it to POST /data/:object))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/spec/src/data/data-engine.zod.ts) — pages documenting those are invisible to this run
  • 1 anchor(s) matched too much of the corpus to be a work list: /data/:object (route, 63 pages)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 136 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 b84a84745aeba97a240681d7742ac9abe5f93719packageMentionDocs.

Which tree this was computed on

This run read content/docs from 32486df103f951ec13adef9a783dd024e8805e8a — the merge of head c7e22addb4cf9435bc3cb401e2a3545f3fce986d into base b84a84745aeba97a240681d7742ac9abe5f93719, 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 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

⚠️ 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 b84a84745aeba97a240681d7742ac9abe5f93719 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-elon-musk
os-elon-musk marked this pull request as ready for review September 18, 2026 12:23
@os-elon-musk
os-elon-musk added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit dbd4744 Sep 18, 2026
37 checks passed
@os-elon-musk
os-elon-musk deleted the claude/issue-18977-orderby-declared-twice branch September 18, 2026 13:05
os-litant pushed a commit that referenced this pull request Sep 18, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling

Projects

None yet

2 participants