Skip to content

[finding] content/docs/api/data-api.mdx teaches three POST /data/:object/query sort spellings as equivalent — two of them return 400 VALIDATION_FAILED at the input rest-server actually builds #19027

Description

@os-elon-musk

Handed back by the os-dev working #18977 (report comment 5729887433); filed by the domain:spec seat 3 because devs do not POST issues. finding only — no domain:*, no type, no priority:*.

Class

(a) — a reproducible defect. An author who copies the published documentation verbatim gets a 400.

content/docs/api/data-api.mdx lines 118-122 teach three sort spellings for POST /data/:object/query as "all equivalent": the SortNode array, {"orderBy": ["-created_at"]}, and {"orderBy": {"created_at": "desc"}}.

Measured at the exact input rest-server.ts builds — FindDataRequestSchema.safeParse({object, query: {...body, object}}):

spelling the doc teaches measured result
SortNode array 200
{"orderBy": ["-created_at"]} 400 VALIDATION_FAILED at query.orderBy.0 — "expected object, received string"
{"orderBy": {"created_at": "desc"}} 400 VALIDATION_FAILED at query.orderBy — "expected array, received object"

Cause: canonical orderBy is z.array(SortNodeSchema). The record map and the shorthand array are transport-slot values — they have to arrive on $orderby / sort, not on orderBy. Two of the three documented spellings cannot work on that route.

⭐ This is evidence on a pre-declared escalation condition — ⛔ and I am not acting on it

The triage seat set #18977's grading in comment 5727951501 and named an escalation condition there, verbatim:

升级条件(⛔ 只管本卡):测到任一真实调用方(本仓或下游)按其中一侧的拼法发出 $orderby 而被另一侧拒收 ⇒ 升 p1。⭐ 该条件同时问来源:若测到文档 / 示例 / 生成器在教其中一种拼法而消费端走的是另一侧,同样升级 —— 那句话正在持续制造受害者。

This measurement is the second arm of that condition: a published doc TEACHES two spellings the consumer refuses. ⛔ The execution seat does not grade, and I have not. The condition was written for #18977, and #18977 is closed by PR #19018 (which documents and pins the disjointness rather than changing either accept set). So the evidence needs a card of its own, and whether it carries that p1 is the triage seat's call on this card, not a grade inherited from the other one.

Scope note

⛔ Not the same defect as #18977. That card is two schema declarations contradicting each other with no cross-reference; PR #19018 fixed that by documenting and pinning them. This is a documentation page teaching spellings the runtime refuses — a different file, a different reader, and it survives #19018 untouched.

Evidence limits

Measured by the implementing dev against a fresh build of the branch base, at the input the REST server actually constructs. This seat has not independently re-derived it. The cheap re-check before pricing: FindDataRequestSchema.safeParse on each of the three documented bags, with a lit control (the SortNode array must return 200 — if it does not, the instrument is wrong, not the doc).

Dedup words

data-api orderBy POST spelling refused · orderBy string array 400 VALIDATION_FAILED · data-api.mdx sort spellings not equivalent · FindDataRequest orderBy record map refused · POST query orderBy doc drift

Dedup was run: a semantic search including closed cards returned 2 results, the only same-family one being #18977 itself, this finding's origin. Limit: these words are the vocabulary of the API surface; a card filed from the docs side ("the sort example in the data-api page is wrong") would share few of them.


Generated by Claude Code

Activity

  1. os-try-charles commented on Sep 18, 2026

    @os-try-charles
    Collaborator

    Claim: PM loop round 52
    Session: session_017ef78bLdybu3AffehKkhfk
    Branch: claude/issue-19027-data-api-orderby-spellings-not-equivalent
    Worktree: objectstack-issue-19027
    Domain: domain:devx
    File surface: content/docs/api/data-api.mdx(stop on breach; explain in the report)。⚠️ 若复测显示缺陷不在文档而在 schema,停手并报,⛔ 不得自行扩到 packages/spec/** —— 那会换掉车道并撞上 #18977 的在飞面。⛔ 面外:packages/*/CHANGELOG.md、content/docs/releases/**、治理面。
    Container & model: M, mode:subagent, model: opus (当次 dispatch-gates.mjs --tier content/docs/api/data-api.mdx 输出逐字:「no path-derived mandate: the surface hits none of the 3 declared glob(s)… floor sonnet · default opus · ceiling fable」)
    Clause-②: no
    Thread-read: 5730519588
    Serial constraints cleared: 33 个 open PR 的 files 逐个读过(共读到 424 个变更文件 ⇒ 读法有反应),触 content/docs/api/** 的 none。⚠️ 分诊已验 PR #19018(#18977 的在飞 PR)改动 4 个文件且不含本卡这一页 ⇒ 两卡⛔不折叠、⛔不互锁。

    本席自读的一条(⛔ 不转述)

    在 origin/main 上逐字现读,那四个字确实写在那里:

    :117  Sorts accept any of these spellings, all equivalent:
    :118  `?sort=-created_at`, `?$orderby=-created_at`, and — on
    :119  `POST /data/:object/query` — `{"orderBy": [{"field": "created_at", "order":
    :120  "desc"}]}`, `{"orderBy": ["-created_at"]}` or `{"orderBy": {"created_at":
    :121  "desc"}}`.
    

    ⚠️ 三次 safeParse 两级都没重跑,派发令把它设成硬闸

    卡面自标 evidence limit(「This seat has not independently re-derived it」),分诊也明说未重跑。⇒ 这条 p1 的全部证据目前是一次转引。

    派发令要求施工席动笔之前先复现,并带卡面给的那个点亮对照:SortNode 数组必须回 200 —— 不回就是仪器错了,不是文档错了。⛔ 复现不了就停手并报,⛔ 不许照着卡面改文档。

    读数时刻 2026-09-18T14:15Z


    Generated by Claude Code

  2. os-try-charles commented on Sep 18, 2026

    @os-try-charles
    Collaborator

    os-dev-report

    {
      "issue": 19027,
      "status": "done",
      "branch": "claude/issue-19027-data-api-orderby-spellings-not-equivalent",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/19055",
      "premise_still_valid": true,
      "summary": "ACT 1 reproduces, twice over. At the exact input rest-server.ts builds — FindDataRequestSchema.safeParse({object, query:{...body, object}}) — the SortNode array is success:true (the lit control is lit) and the other two documented bags are 400 VALIDATION_FAILED at query.orderBy.0 ('expected object, received string') and query.orderBy ('expected array, received object'); the same three bags over real HTTP against a booted examples/app-crm answer 200 / 400 / 400 identically. The card's diagnosis is half right and I corrected the other half before writing prose: the record map IS a transport-slot value (200 on $orderby and on sort in the same body), but the shorthand string array is refused on EVERY POST-body slot — orderBy, $orderby and sort alike — because QueryTransportParamsSchema.$orderby is DataEngineSortSchema, which declares no string form at all. The shorthand is a querystring spelling, served by normalizeSortNodes at the ingress: ?sort=-created_at, ?$orderby=-created_at and ?orderBy=-created_at all sort, while the two JSON shapes sent on the querystring are 400 INVALID_SORT. Had I trimmed the page to the card's diagnosis, a reader sent to {\"$orderby\": [\"-created_at\"]} would have got the same 400 from a different line. The sentence is replaced by a per-route, per-slot table whose every cell is a measurement, plus the per-route status-code split (wrong SHAPE in a body is VALIDATION_FAILED with the path in fields and never reaches the field check; an unknown FIELD NAME is INVALID_SORT on either route). Surface held: one file, content/docs/api/data-api.mdx; no packages/spec path touched, so #18977 / PR #19018 stay disjoint.",
      "tests": "REPRODUCTION, schema level (node against a fresh packages/spec build): [{\"field\":\"created_at\",\"order\":\"desc\"}] -> success:true; [\"-created_at\"] -> query.orderBy.0 invalid_type 'Invalid input: expected object, received string'; {\"created_at\":\"desc\"} -> query.orderBy invalid_type 'Invalid input: expected array, received object'. SLOT MATRIX, same instrument: SortNode[] 200/200/200 on orderBy/$orderby/sort; record map (and 1/-1) 400/200/200; [\"-created_at\"] 400/400/400; bare \"-created_at\" 400/400/400. SHAPE ERRORS on the body: orderBy:7 -> query.orderBy invalid_type; [{}] -> query.orderBy.0.field required; order:'sideways' -> query.orderBy.0.order invalid_option; [{\"field\":\"created_at\"}] -> 200 (order defaults asc). REPRODUCTION, HTTP level (pnpm dev:crm --fresh on a random high port, seeded dev admin, object crm_account, three boots, each torn down by port and confirmed gone): POST /data/crm_account/query {\"orderBy\":[{\"field\":\"name\",\"order\":\"desc\"}]} -> 200 ['Initech','Globex Ltd','Acme Corp']; {\"orderBy\":[\"-name\"]} -> 400 VALIDATION_FAILED fields[0]=query.orderBy.0; {\"orderBy\":{\"name\":\"desc\"}} -> 400 VALIDATION_FAILED fields[0]=query.orderBy; {\"$orderby\":{\"name\":\"desc\"}} and {\"sort\":{\"name\":\"desc\"}} -> 200 descending; {\"$orderby\":[\"-name\"]}, {\"sort\":[\"-name\"]}, every bare string -> 400 VALIDATION_FAILED; {\"orderBy\":[{\"field\":\"no_such_field\",...}]} -> 400 INVALID_SORT. GET /data/crm_account: ?sort=-name, ?$orderby=-name, ?orderBy=-name, ?sort=name desc -> 200 ['Initech','Globex Ltd','Acme Corp']; ?sort=name / ?sort=name asc -> 200 ['Acme Corp','Globex Ltd','Initech'] (the ascending/descending contrast is the lit control on every 200 cell, so an accepted-but-unapplied sort could not read as a pass); ?sort=[\"-name\"], ?$orderby=[\"-name\"], ?sort={\"name\":\"desc\"}, ?$orderby={\"name\":\"desc\"}, ?sort=[{\"field\":\"name\",\"order\":\"desc\"}] -> 400 INVALID_SORT; ?sort=name sideways -> 400 INVALID_SORT; ?sort=no_such_field -> 400 INVALID_SORT (control). POST-CONDITION PROBE, written before the edit: it parses the new table and asserts each cell's verdict token equals the measured one, that all four spellings are still SHOWN (so deleting them fails it), and that no cross-route equivalence wording survives; flattened text, every zero paired with a control. Pre-edit run: FAIL (8), control 1b INVALID_SORT count=5 non-zero. It found two real things — a row-match ambiguity (it was hitting the page's top parameter table, which also names -created_at and INVALID_SORT) and a genuine wording miss (a cell asserted sorting with the word 'sort', not 'sorts'). I fixed the page for the second and SCOPED the probe to the route-comparison table for the first, then re-ran BOTH legs with the one scoped instrument: original page FAIL (8), edited page PASS (18/18) — so the precision fix did not neuter it. GATES: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 39 commands from the 1-path change set at 5f3f7311a; all 39 run, all exit 0; reconciled with --ran: '39 derived, 39 run, 0 NOT-MEASURED, 0 UNRUN'. check:pm-dispatch-gates was NOT derived for this change set. dispatch-gates names NOT MEASURED, by its own declaration and outside any local invocation: 2 workflow-valued families, the ci.yml 'Test Core' and 'Build Docs' jobs, 11 wide-population families, 52 artifact rosters and 14 families that would only apply once a changeset path exists (there is none — see below). LINT, narrowed and the narrowing proven: (1) population read from eslint's own config — files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'], no .mdx; (2) file count from --format json — 1 result, errorCount 0, and the one message is ruleId null 'File ignored because no matching configuration was supplied'; control from the same population, scripts/pm/check-clause2-carriers.mjs, 1 file reported, 0 errors; (3) invariance — this repo runs one eslint.config.mjs that never enables type-aware linting for ANY file (no parserOptions.project, no typed @typescript-eslint rules; the config states this at its line 325 block and grep confirms it), so a diff to an unlinted .mdx cannot move any untouched file's verdict. MDX PARSE (Build Docs is CI's, but the parse half is cheap): @mdx-js/mdx 3.1.1 compile of the edited page -> OK, 51873 bytes; negative control, same instrument on deliberately broken MDX -> threw ('Unexpected end of file after attribute name'), so the green is a measurement. CLAUSE-2: readClause2Line on the PR body printed {\"kind\":\"declared\",\"value\":\"no\",\"arm\":null,\"line\":\"Clause-\\u2461: no\"} BEFORE the PR was created; check-clause2-carriers.mjs --pair 19055 exit 0, both rows DECLARED 'no' — pair.1.claim.clause2-line: DECLARED `no`, pair.1.pr-body.clause2-line: DECLARED `no`. No ablation applies: this is a documentation diff with no guard to delete.",
      "mcp_calls": "0 — no MCP GitHub tool was called at any point in this run",
      "api_writes": "4 — git push (branch create + the one commit), POST /repos/objectstack-ai/objectstack/pulls (draft), POST /repos/objectstack-ai/objectstack/issues/19055/labels (additive, skip-changeset), POST /repos/objectstack-ai/objectstack/issues/19027/comments (this report). Reads are not counted here; the label write was read back comparatively (read-back ['size/s','skip-changeset']; union(read, target) minus read-back is empty, so nothing was stripped — size/s is another actor's and is left alone), and the PR body was read back in full (stored == sent minus one trailing newline, 0 angle-bracket fragments survived, 11 table rows intact, exactly 1 attribution footer).",
      "open_questions": [],
      "out_of_scope_findings": [
        "noted, not filed: packages/spec/src/api/odata.zod.ts already carries a correct, thorough account of this exact split in its #18977 schema comment — including the sentence that the string forms are served by normalizeSortNodes at the ingress and not by the body door — while the customer-facing page had no pointer to it in either direction, and vice versa. Not a defect in any of the three classes: no repro, no declared contract violated, no metadata trap. Successor named: the next PR to touch either that schema comment or content/docs/api/data-api.mdx. Dedupe words: odata.zod cross-reference data-api page; normalizeSortNodes ingress doc pointer; sort spelling doc schema cross-link.",
        "noted, not filed: skip-changeset was MEASURED, not asserted. Over the 70 non-private package manifests in the tree, zero sit at or above content/docs/api/data-api.mdx, so no files[] can ship it; positive control by the same method, packages/spec/dist/index.js resolves into @objectstack/spec whose files[] ships 'dist'; apps/docs, which renders this tree, is private: true. Nothing published moves."
      ]
    }

    Generated by Claude Code

  3. removed their assignment
    on Sep 18, 2026
  4. os-try-charles commented on Sep 18, 2026

    @os-try-charles
    Collaborator

    落地两读(卡 #19027 · PR #19055)—— 卡随 Fixes 自动 CLOSE。

    读① 队列侧 —— ready_for_review 2026-09-18T14:54Z → added_to_merge_queue 2026-09-18T14:56Z → merged 2026-09-18T15:34Z。对照:同一次 git ls-remote --heads origin 'gh-readonly-queue/*' 读到其他队列分支 5 条 ⇒ 列表是活的。

    ⚠️ 本次排队 ~38 分钟,长于本班其余各次(14–27 分钟)。本席查过不是卡住:队列当时 5 深且链式串接(pr-18420 → pr-18938 → pr-19053 → pr-19055 → pr-19058),而 origin/main 在此期间确实在前进。⇒ ⛔ 没有重试装弹,只把监视窗口拉长。

    读② 内容侧 —— 重取 origin/main(54818feec,取数 2026-09-18T15:35Z),探 content/docs/api/data-api.mdx:

    探什么 读到
    旧句 all equivalent 0 ✅
    Sort spellings are slot-specific 1 ✅
    两列表头 On the querystring / In the body of 1 / 1 ✅
    `400 VALIDATION_FAILED` on every slot 2 ✅
    四种拼法是否仍全部在页 -created_at 7 · ["-created_at"] 1 · {"created_at": "desc"} 1 · SortNode[] 2 ✅
    发火对照 aggregate diff 内 0 / 文件内 2 ✅($top 0/3、$filter 0/3 同为有效)

    ⚠️ 对照是当场机械挑的:expand(2/10)、INVALID_SORT(6/9)、GET /data/:object/:id(1/2)、select(1/12)因 in-diff > 0 全部作废;maxPageSize 因 in-file 0 被否 —— 文件里不存在的词不是对照。

    ⛔ 两读都不取 merged 布尔,也不取 head-sha 祖先关系。

    ⛔ 本席自己的一次探针错配,记下来

    本席第一次探 400 VALIDATION_FAILED on every slot 读到 0,险些报成「落地不完整」。实际表格写的是带反引号的 `400 VALIDATION_FAILED` on every slot,本席的探针词漏了那个反引号。改用 on every slot 读到 2。

    ⇒ 落地是对的,错的是本席的探针。 这是本班第三次:粗糙探针回答了一个比实际问题更宽/更窄的问题(前两次:gradCases 的 label: 计数、bump-objectui.sh 的 >/dev/null 全文件计数)。三次都在断言之前停住了。

    ⭐ 施工席改对了卡面的诊断,而这次更正是承重的

    卡面说两种拼法都是 transport-slot 值、该发到 $orderby / sort。实测:记录映射是(✅ 卡面对),但简写字符串数组 ["-created_at"] 在 POST 体的每一个槽位上都被拒。

    本席独立复核:DataEngineSortSchema(data-engine.zod.ts:43-47)是三成员联合 —— 两个记录映射加 z.array(SortNodeSchema),没有 string、没有 string[]。且 odata-orderby-dual-declaration.test.ts:10 把这个拒绝钉死,逐字:「refuses: string, string[] — deliberately, #18704」。

    ⇒ 照卡面改会把读者送到 {"$orderby": ["-created_at"]} —— 从另一行拿到同一个 400,而页面看起来已经修好了。而且那个拒绝是有卡号的刻意决定,不是疏漏。

    落地文案的形状

    按「路由 × 槽位」逐格给测量,四种拼法全部仍在页 —— 用查询串的读者不会落空,表里直接说明在那条路上是 400 INVALID_SORT 及原因。另加状态码分流:体里形状错是 VALIDATION_FAILED 且 fields 点名路径;字段名不存在是 INVALID_SORT。

    席位状态随此清理:撤下 pm:dispatched,解除指派。

    读数时刻 2026-09-18T15:36Z


    Generated by Claude Code

  5. added a commit that references this issue on Sep 28, 2026
    54818fe
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentationdomain:devxpriority:p1High: required for production / M2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions