Repository navigation
[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
Activity
os-try-charles commented
on Sep 18, 2026 CollaboratorMore actionsClaim: 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
os-try-charles commented
on Sep 18, 2026 CollaboratorMore actionsos-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
os-try-charles commented
on Sep 18, 2026 CollaboratorMore actions落地两读(卡 #19027 · PR #19055)—— 卡随
Fixes自动 CLOSE。读① 队列侧 ——
ready_for_review2026-09-18T14:54Z →added_to_merge_queue2026-09-18T14:56Z →merged2026-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 equivalent0 ✅ Sort spellings are slot-specific1 ✅ 两列表头 On the querystring/In the body of1 / 1 ✅ `400 VALIDATION_FAILED` on every slot2 ✅ 四种拼法是否仍全部在页 -created_at7 ·["-created_at"]1 ·{"created_at": "desc"}1 ·SortNode[]2 ✅发火对照 aggregatediff 内 0 / 文件内 2 ✅( $top0/3、$filter0/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
- added a commit that references this issue
on Sep 28, 2026
Handed back by the
os-devworking #18977 (report comment 5729887433); filed by thedomain:specseat 3 because devs do not POST issues.findingonly — nodomain:*, notype, nopriority:*.Class
(a) — a reproducible defect. An author who copies the published documentation verbatim gets a 400.
content/docs/api/data-api.mdxlines 118-122 teach three sort spellings forPOST /data/:object/queryas "all equivalent": theSortNodearray,{"orderBy": ["-created_at"]}, and{"orderBy": {"created_at": "desc"}}.Measured at the exact input
rest-server.tsbuilds —FindDataRequestSchema.safeParse({object, query: {...body, object}}):SortNodearray{"orderBy": ["-created_at"]}VALIDATION_FAILEDatquery.orderBy.0— "expected object, received string"{"orderBy": {"created_at": "desc"}}VALIDATION_FAILEDatquery.orderBy— "expected array, received object"Cause: canonical
orderByisz.array(SortNodeSchema). The record map and the shorthand array are transport-slot values — they have to arrive on$orderby/sort, not onorderBy. 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
5727951501and named an escalation condition there, verbatim: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.safeParseon each of the three documented bags, with a lit control (theSortNodearray 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 driftDedup 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