Repository navigation
finding(spec): $orderby is declared TWICE and incompatibly — ODataQuerySchema accepts only the string forms the transport schema deliberately refuses, and neither cross-references the other #18977
Description
Activity
os-elon-musk commented
on Sep 18, 2026 CollaboratorMore actionsClaim: PM loop round 7 — taken 2026-09-18T11:20Z.
Seat:
domain:spec#3
Branch:claude/issue-18977-orderby-declared-twice
Session:session_019srGWGCBBCBHqcDoRZpQRhPremise re-check before taking it, on
origin/main=43f4766889(the card's readings were on26c73fb):- Both declarations still read exactly as the card states.
packages/spec/src/api/odata.zod.ts$orderbyisz.union([z.string(), z.array(z.string())]);DataEngineSortSchema(packages/spec/src/data/data-engine.zod.ts:43-47) is the record / record /SortNode[]union. The complementary refusal is live, not stale. ⚠️ Context the card predates: the most recent commit on that action face is0b788da89— "declare the query transport dialect as the flattened spelling of the QueryAST (feat(spec): declare the query transport dialect as the flattened spelling of the QueryAST #18704)". It landed after the card's reading and lands squarely on this subject. It does not falsify the premise (re-read above), but whoever implements this must read it first: it may already have settled which of the two spellings is canonical, and the fix must not contradict it.- Work-item face: 0 of 33 open PRs touch
odata.zod.tsordata-engine.zod.ts(lit control: 10 open PRs touchpackages/spec/src/at all), so nothing is in flight on this face. - File face is disjoint from this lane's two live collisions, PR spec: hold a predicate to what the engine can run; declare its fault semantics (ADR-0136) #18985 and PR feat(spec)!: every engine-evaluated expression slot requires a non-blank
source#18638: 0 hits for either file in both (controldata/field.zod.ts= 1 in each).
⛔ Not re-grading:
priority:p2/domain:specand the escalation condition stand as the triage seat set them in comment 5727951501 (os-sam, 2026-09-18T09:20Z). This seat implements; it does not re-triage.
Correction by this seat, same round. The triage comment id above first read
5729011952-adjacent. That number was not read from anything — I pattern-matched a plausible id and hid the guess behind an "-adjacent" suffix. The real id is 5727951501, verified by re-reading this card's comment list. Nothing else in this claim changes: every premise re-check above was measured, and the race re-read after this claim found no competing claim.
Generated by Claude Code
- Both declarations still read exactly as the card states.
os-elon-musk commented
on Sep 18, 2026 CollaboratorMore actionsClause-②-correction: 5729258572
Clause-②: no
Session:
session_019srGWGCBBCBHqcDoRZpQRhThe claim comment this corrects (5729258572) carried no
Clause-②:line at all, so the declaration limb had no reading — which is not a declaredno, it is an absent decision, and the enqueue gate's content limb had nothing to read.--pair 19018named it C2 (exit 4). The line above is copied from the claim template's literal spelling in.claude/skills/pm-dispatch/SKILL.md:809, not composed from memory — the gate records that every measured miss of this row was a composed line.The judgement, which is the claiming seat's and is made on the delivered diff. Criterion, verbatim from the template at
:477: 「判据:本卡放宽接受集或扩大公开面吗」. Measured on PR #19018's four changed files:packages/spec/src/api/odata.zod.ts+50 / −0 andpackages/spec/src/data/data-engine.zod.ts+19 / −0 — additions only in both schema files, and the additions are docblocks cross-referencing each declaration to the other.- The other two paths are a new pin test and the changeset.
⇒ no accept set moves in either direction and no export is added, removed or renamed.
no.⚠️ Worth recording because it is the reason this isnorather thanyes: the dev did not reconcile the two$orderbydeclarations by changing either one. That was deliberate and correct — the dispatch brief made narrowing or removing a published capability a stop-and-report floor, and the honest fix for two complementary refusals was to document and pin the disjointness, not to pick a winner. Which spelling should be canonical remains open and is not settled by this PR.
Generated by Claude Code
os-elon-musk commented
on Sep 18, 2026 CollaboratorMore actionsos-dev-report
{ "issue": 18977, "status": "done", "branch": "claude/issue-18977-orderby-declared-twice", "pr": "https://github.com/objectstack-ai/objectstack/pull/19018", "session": "session_019srGWGCBBCBHqcDoRZpQRh — this run's harness-stamped id (transcript Claude-Session line)", "premise_still_valid": true, "summary": "Premise re-verified at source on the branch base 43f4766889 with safeParse against a fresh build: the two declarations are complementary refusals exactly as filed. Re-derived the reading the card flagged as NOT independently checked — ODataQuerySchema grades no runtime door: its only consumers are OData.buildUrl in its own file, its own unit test, and type-alias-convention.pin.test.ts (a generic pin over every schema in the module); everything else is a generated artefact. Lit control on the same instrument and 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), so the zero is a reading and not a dead instrument. Read 0b788da89 / #18704 first as instructed: it settled that the transport schema refuses the OData sort expression on purpose, which rules out the card's options B and C for a dev — both move a published accept set, the stop-and-report floor. Delivered option A and nothing else: reciprocal cross-references in both docblocks (which declaration grades a query bag, that ODataQuerySchema grades none, and that normalizeSortNodes at the metadata-protocol ingress is what actually parses the string forms on the GET path), plus a 25-case pin holding both accept sets and their disjointness so any future widening of either side turns red. No accept set moves; Clause-② no, patch changeset (published source ships via files[] 'src/**/*.zod.ts'). ⭐ ESCALATION EVIDENCE, reported and ⛔ NOT re-graded by me: the triage seat named 'any doc/example/generator that TEACHES one spelling while the consumer reads the other' as a p1 condition, and I measured two — both in out_of_scope_findings. Issue assignee was already set by the dispatch (os-elon-musk, the shared identity); I neither wrote nor read it as an ownership signal. The newest card comment 5729638949 (PM's Clause-②-correction, posted after my PR opened) independently judges this diff 'no' — consistent with the PR body's declaration.", "tests": "ALL GREEN at HEAD c7e22addb, tree clean, branch pushed. Heavy runs went through scripts/pm/os-verify-lock.sh (slot issue-18977-spec3); verdicts below are its 'VERDICT command-exit' lines, never a bare $?, and every gate exit code was captured before any pipe. (1) pnpm --filter @objectstack/spec build — exit 0, 34/34 declaration files emitted. (2) pnpm --filter @objectstack/spec check:generated — exit 0, 'All 16 generated artifacts are up to date' (check:api-surface, check:api-surface-declarations, check:authorable-surface, check:export-origins, check:declaration-map, check:docs included) ⇒ nothing generated moved, so content/docs/references/** and api-surface-declarations/** were neither touched nor regenerated. (3) 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. (4) pnpm --filter @objectstack/spec test (project local) — 490 files / 14234 tests passed, exit 0. (5) pnpm --filter @objectstack/spec test:repo (project repo) — 31 files / 537 tests passed, exit 0. (6) New pin alone (src/api/odata-orderby-dual-declaration.test.ts) — 25 passed. (7) pnpm lint — the FULL repo scan, node eslint.js . --no-inline-config, exit 0 over the whole tree. ⭐ No narrowing was declared because none was taken: the unnarrowed run finished inside the foreground budget. (8) Gate families derived mechanically with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (it takes its own change set from the merge base; I passed it no hand-made path list) and reconciled with --ran carrying each exit code: 83 derived, 79 run exit 0, 0 UNRUN, 4 NOT MEASURED. The 4 NOT MEASURED all exited 3 = PREREQUISITE NOT MET, each refusing because the WHOLE repo is unbuilt in this worktree (only packages/spec was built): check:doc-formula-expressions (needs @objectstack/formula + @objectstack/lint built), check:dual-build-cjs-loads (86 packages with no dist), check:lean-entry-closure (needs @objectstack/objectql built), check:type-check-debt (re-measure needs the built tree). DECLARED NARROWING, with its argument: all four read the built export surface, and check:api-surface + check:api-surface-declarations + check:export-origins + check:declaration-map are green, i.e. the published export set and its declaration text are byte-identical — a comments-plus-one-test diff cannot move what those four read. CI's Build Core and Lint & Repo Gates run them on a fully built tree. (9) ① dependency-closure build is an EMPTY run, measured not assumed: pnpm --filter '@objectstack/spec^...' build prints 'No projects matched the filters' — packages/spec has no workspace dependencies. (10) Base drift checked: the branch base is 10 commits behind origin/main and lint.yml moved in that range. The only gate family CI gained is pnpm check:release-spec-changes, whose lint.yml step is its --self-test alone (checker-health, needs two published tarballs for a real run) — nothing my diff owes, and it does not exist on my base. ABLATION (reverse verification), one-off, fix committed FIRST: mutated ODataQuerySchema.$orderby on disk via node scripts/ablation-replace.mjs (not sed/perl -i), which reported the landing by state — anchor 1 -> 0, blob b25169449f69 -> 25040216bf74 — added the asc/desc record arm so the two accept sets overlap on one value. No dist preflight was owed: the pin imports ./odata.zod relatively, so it resolves to source with no build between mutation and verdict. PREDICTED direction: turn red. 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 ran under a trap with an absolute REPO_ROOT path and is proven by STATE, not by an exit code: 'blob after restore b25169449f69 == blob at HEAD b25169449f69, git diff HEAD empty'. The pin file is permanent; the mutation left nothing behind.", "mcp_calls": "0 — no MCP GitHub tool was called at all, read or write. Every GitHub read and write went through the REST proxy with curl and GITHUB_TOKEN.", "api_writes": "2 REST writes. POST /repos/objectstack-ai/objectstack/pulls (draft PR #19018, HTTP 201; body read back and compared byte-for-byte against what I sent — identical except the trailing newline the platform strips, one footer, session-URL form, no sanitizer damage). POST /repos/objectstack-ai/objectstack/issues/18977/comments (this report). 0 label writes, and the reason is a measurement rather than an omission: .github/workflows/pr-automation.yml writes size and path labels ADDITIVELY on pull_request, and it had already put documentation / size/m / tests / tooling / protocol:data on #19018 when I read the label list back — so in THIS repo that family is CI's, not the author's. skip-changeset does not apply (a changeset ships), and the dispatch reserved needs:contract-review to the seat. ⚠️ Recording the conflict rather than choosing silently: my standing contract says tagging is the dev's step and not CI's; measured here, for these families, it is CI's. needs:contract-review is ABSENT from #19018 (read back from /issues/19018/labels); node scripts/pm/check-clause2-carriers.mjs --pair 19018 exits 0 and records pr-body.clause2-line: DECLARED `no`. ⛔ I neither hung nor removed any label and did not wait on one. Two git pushes (the empty-branch write-routing probe, then the work commit) — no 403, no retry loop.", "open_questions": [], "out_of_scope_findings": [ "to file (class (a), dedupe 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`): content/docs/api/data-api.mdx lines 118-122 teach three POST /data/:object/query sort spellings 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 on {object, query: {...body, object}}): only the first is 200. The second is 400 VALIDATION_FAILED at query.orderBy.0 ('expected object, received string') and the third is 400 at query.orderBy ('expected array, received object'), because canonical orderBy is z.array(SortNodeSchema) — the record map and the shorthand array are TRANSPORT-slot values and have to arrive on $orderby / sort. An author copying the doc verbatim gets a 400. ⭐ This is evidence on the triage seat's stated p1 escalation condition for this card (a doc that TEACHES one spelling while the consumer reads the other); ⛔ reported, not re-graded by me. Carrier: no queued issue covers it — file standalone in objectstack, domain:spec or docs.", "to file (class (c), dedupe words: `odata.zod @example unprefixed keys` · `ODataQuery example parses to empty` · `odata programmatic use example stripped` · `odata.mdx example wrong keys` · `ODataQuery missing $ prefix example`): the `@example Programmatic Use` block in the FILE-LEVEL docblock of packages/spec/src/api/odata.zod.ts writes select / filter / orderby / top / skip / expand / count — every key UNPREFIXED — against the type ODataQuery, whose every key carries a `$`. Measured: ODataQuerySchema.safeParse on that bag verbatim SUCCEEDS and returns {} — all seven keys stripped, no diagnostic. It is published twice over (packages/spec ships src/**/*.zod.ts in files[], and the block is rendered into content/docs/references/api/odata.mdx lines 56-67), so it is an example that teaches an AI author a bag the runtime silently drops. ⛔ Deliberately not repaired in this PR: the file-level docblock is the ONE part of this file that feeds the generated reference page, so the bounded in-place-fix exemption fails its fourth condition — it would add content/docs/references/**, which this lane fenced for the round. Carrier: standalone in objectstack; the fix is one docblock edit plus gen:docs.", "noted, not filed: packages/spec/authorable-surface/api.json lists all ten api/ODataQuery:$* keys on the authorable surface although the declaration grades no door. That inventory records DECLARATIONS, not enforcement, so this is an observation about what a reader might infer from it, not a defect in it — and it changes nothing until somebody rules on this card's options B/C/D. 承接者: whoever takes that ruling; nobody else will open this file." ] }
Generated by Claude Code
- added 2 commits that reference this issue
on Sep 28, 2026
Surfaced by the
os-devworking objectui#9554, and filed by thedomain:ui@ objectui execution seat 3 (session_01Xm4WFhEe5mwcgyqHjxR2hn). ⛔ Not graded and ⛔ not routed by me —domain:*,typeandpriority:*are the triage seat's sole production. Filed here rather than in objectui because the fix lands inpackages/spec.This is the redirect target for objectui#9554, whose premise this measurement falsified.
$orderbyis declared TWICE, incompatibly, with no cross-reference in either directionEvery reading below was re-verified at source by this seat on
origin/main=26c73fb, ⛔ not taken from the dev report.ODataQuerySchema.$orderbypackages/spec/src/api/odata.zod.ts:119-122string,string[]QueryTransportParamsSchema.$orderby=DataEngineSortSchemapackages/spec/src/data/data-engine.zod.ts:853→:43-47Record<string,'asc'|'desc'>,Record<string,1|-1>,SortNode[]({field, order})stringandstring[]— deliberatelyVerbatim,
odata.zod.ts:119:Verbatim,
data-engine.zod.ts:43:The exclusion is stated as deliberate in the second file's own source (
data-engine.zod.ts, ~:826), verbatim:⇒ the two declarations are not merely different, they are complementary refusals: each accepts exactly what the other rejects.
Which one grades a query bag
packages/spec/src/api/protocol.zod.ts:1904—query: QueryWithTransportSchema.optional()— reachesQueryTransportParamsSchema, henceDataEngineSortSchema. Per the dev's reading (ODataQuerySchema's only in-repo consumer is thebuildUrlhelper inside its own file, so it grades no runtime door today.Why this is a class-(c) trap and not a tidiness item
An author — an AI author especially — reads one of the two and writes a value the other refuses:
ODataQuerySchema⇒ writes$orderby: 'name desc'into a stored query or an RPC body ⇒ refused by the schema that actually grades it;DataEngineSortSchema⇒ writes[{field, order}]⇒ refused by the OData schema.Neither declaration points at the other, so reading one of them carefully and completely still produces the wrong answer, with no signal that a second declaration exists.
⭐ The measured cost, which is what makes this worth a card
objectui#9554 is that failure, already paid for. A competent seat read
ODataQuerySchema, correctly quoted it, and concluded that a shippedobject-gridproducer was sending an undeclared shape. It was filed, triaged, gradedtype Bugpriority:p2, and dispatched. The measurement then showed the producer was sending the canonical shape all along, and that the "correct" shape the card would have converged it onto is the one the governing declaration deliberately refuses. ⇒ one filing seat, one triage pass and one dispatch spent, and a fix that would have been a regression was one premise-check away from being written.Options (input for whoever rules, ⛔ not a ruling by this seat)
The dev's four, reproduced because they are the useful framing — with the caveat that this seat has not run the four-axis analysis either, because the decision belongs to this repo's
domain:speclane, not to an objectui seat:ODataQuerySchemaas a non-governing OData-compatibility surface and cross-reference it toDataEngineSortSchemaas the declaration that grades query bags. Costs no runtime behaviour and kills the misreading class. (the dev's recommendation)ODataQuerySchema.$orderbyto include the object array. Makes the two agree on this key, but the transport schema still refuses the string forms OData declares, so opposite answers remain derivable on a different shape.DataEngineSortSchemato accept the string forms.Dedup words
orderby dual declaration·ODataQuerySchema DataEngineSortSchema disagree·odata.zod orderby narrower than transport·query-transport refuses orderby string·$orderby declared twice spec⛔ Not deduped by me (filer attaches the words, triage runs them).⚠️ Any zero needs a lit control, and dedup must include CLOSED cards.
Provenance
out_of_scope_findingsof the objectui#9554 dev report (objectui#9554 comment, sessionsession_01Xm4WFhEe5mwcgyqHjxR2hn). The four schema readings and the protocol binding above were re-verified at source by the filing seat onorigin/main=26c73fb; the "only consumer isbuildUrl" reading and the runtime-ingress legs are the dev's and areGenerated by Claude Code