Repository navigation
fix(spec)!: the published list-query schemas declare the page window the Data API enforces - #25300
Conversation
Claude-Session: https://claude.ai/code/session_0153NL7xGym5Dpyb4sQbsArM Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153NL7xGym5Dpyb4sQbsArM Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153NL7xGym5Dpyb4sQbsArM Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153NL7xGym5Dpyb4sQbsArM Co-authored-by: Claude <noreply@anthropic.com>
…main Claude-Session: https://claude.ai/code/session_0153NL7xGym5Dpyb4sQbsArM Co-authored-by: Claude <noreply@anthropic.com>
…e-window sites Claude-Session: https://claude.ai/code/session_0153NL7xGym5Dpyb4sQbsArM Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153NL7xGym5Dpyb4sQbsArM Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153NL7xGym5Dpyb4sQbsArM Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check12 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 145 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 d62cfdd4f607579163fa5dc59817007c2dbf8b1b && git checkout d62cfdd4f607579163fa5dc59817007c2dbf8b1b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f47f7f222f1c8c03ab866efa428f4b50ff72b9b8 82fb363acae6bdb5cfb6dbaef380036f54e2647c && git checkout -B drift-repro f47f7f222f1c8c03ab866efa428f4b50ff72b9b8 && git merge --no-ff 82fb363acae6bdb5cfb6dbaef380036f54e2647c
node scripts/docs-audit/affected-docs.mjs --json f47f7f222f1c8c03ab866efa428f4b50ff72b9b8 |
Contract reviewServed-tier: Inputs: card #25269 (body, claim 6349444847, os-dev-report 6351947108), PR #25300 (body, 16-file list, net diff against ① Derived judgmentsEach accept-set or public-surface change the diff implies, named right or wrong against the code at the head.
② Semver level
③ Boundary flagsOpen question (dev recommends A) — A, decided by the rules. The card's acceptance line reads "no door's runtime answer changes (the server already refuses these)". Declaring the window on
Out-of-scope findings (os-dev-report 6351947108):
Other flags. None of the three needs the maintainer. The PR touches no governed surface, is 467 changed lines, and its head repo is the base repo. Pending at review time: Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对ACCEPT ·
Generated by Claude Code |
Requested by Bill · project thread
Fixes #25269
Clause-②: no (narrowing)
Metadata-protocol: none
What changes
The Data API list doors refuse a page window that is not a non-negative integer (PR #25260, one normalizer in
findData). The published request schemas for those doors still declared any number, so a client generated from them was told-1,1.5or a blank?top=was valid. Now they declare what the door enforces, from one declaration.packages/spec/src/data/data-engine.zod.ts):QueryPageWindowSchema(z.number().int().nonnegative(), which in zod 4 is exactlyNumber.isSafeInteger(n) && n >= 0) andQueryPageWindowParamSchema(that number, or a non-blank string whoseNumber(...)is one, parsed to the count). Type aliases per ADR-0122.packages/metadata-protocol/src/protocol.ts,pageWindowValue): the window is judged byQueryPageWindowParamSchema.safeParseinstead of a restated predicate.metadata-protocolalready imports the spec's transport tables from@objectstack/spec/data, so there is no layering inversion. Same accept set, same refusal text and code.FindDataRequestSchema(thePOST /data/:object/querybody, throughQueryWithTransportSchema): the canonicallimit/offset/topmembers declare the page window; the transport spellings$top/$skip/skip(QueryTransportParamsSchema) declare it on their number arm, and the fold's string lowering judges the digits byQueryPageWindowParamSchema(it used to accept any finite number).QuerySchemaitself is not touched (see Measured).HttpFindQueryParamsSchema(theGET /data/:objectquerystring declaration):top/skipdeclareQueryPageWindowParamSchemainstead ofz.coerce.number(), which admitted-1,1.5and a blank value (as0).api-surface,export-origins,declaration-map,json-schema.manifest, and two new rows indropped-refinements.baseline.json(the string arm's refine has no JSON Schema form; itsNumbersemantics cannot be written as a pattern without approximating it, so it is declared there as the ledger asks).Measured: which schemas a door actually reads (base
8dfd1265f, re-checked after mergingbda53bb0b)The normalizer judges the top-level
limit/offsetof afindDatacall (pageWindowValue, two call sites).findDatacallers:GET /data/:object(querystring, no schema),POST /data/:object/query(validatesFindDataRequestSchemafirst), the runtime dispatcher's twins (same schema onPOST), declaredobject_operationendpoints, MCPquery, actioncallData('query'), and server-built windows (import runner, connector pull, export loop: all integers).FindDataRequestSchema→QueryWithTransportSchema/QueryTransportParamsSchema(limit,offset,top,$top,$skip,skip)POST /data/:object/queryand the dispatcherPOSTvalidate itHttpFindQueryParamsSchema(top,skip)GET /data/:object(no runtime parse; the SDK reads it as the declaration)EngineQueryOptionsSchema(data-engine.zod.ts:111/114/117)engine.findoptions; no door parses it,ObjectQL.finddoes not validate windows, and in-process callers reach it without the normalizerQuerySchema(limit/offset/top)QuerySchemais also the driver contract and the recursiveexpandvalue, whose windows the normalizer never judgesget_record.limit(builtin-node-config.zod.ts:628)dataservice (ObjectQL,IDataEngine.find/findOne), notfindDataListRecordResponseSchema.pagination(contract.zod.ts:445/446)AuditMetaItemRequest/HistoryMetaItemRequest/ListNotificationsRequestlimit(protocol.zod.ts:1823/1921/3281)DataEngineFindRequestSchema(RPC)So
Metadata-protocol: none: no authored metadata window is judged by the normalizer, and the two narrowed schemas are request schemas. No migration line is owed for an authored flowquerywindow:get_recordnever reaches the normalizer, so itslimitis not narrowed here.Producer census (fractional or negative windows):
examples/**on this tree, no hit; HotCRM at4166f2146bec, 41 files nameget_record, every literallimitis a positive integer (1, 500, 1000, andBILLING_HANDOFF_LINE_ITEM_LIMIT = 200), no fractional or negative window anywhere.One door answer moves: the code, not the verdict
POST /data/:object/queryvalidates its body againstFindDataRequestSchemabeforefindData, so now that the schema declares the window, a malformed one is refused there:400 VALIDATION_FAILEDwithfields[0].field=query.limit/query.offset, where it was400 INVALID_REQUESTfrom the normalizer. It is still a 400 before any read, and it is the same answer the door already gave{"limit":"ten"}. The runtime dispatcher'sPOSTtwin moves the same way, by reading: it asks the same schema and answersrequestSchemaRefusal, aVALIDATION_FAILED.GET /data/:objecthas no body schema and is byte-unchanged. Nothing newly refused, nothing newly accepted.packages/rest/src/data-page-window-door.test.ts'sPOSTpin is updated to assert this. See the open question in the report.Tests
packages/metadata-protocol/src/protocol.page-window-declared.test.ts(new). The oracle is the door: each value goes through the realfindDatawith a recording engine (accepted iff the engine is reached; the window it receives is the expected parse output). 13 schema rows (FindDataRequest× 6 spellings,QueryWithTransport× 3,HttpFindQueryParams× 2, the two exported schemas) × 17 numbers, 18 strings and 3 other values (-0,MAX_SAFE_INTEGERand one past it,NaN,±Infinity,' 5 ','1e2','0x10', blank, whitespace,'-1','1.5',true,{}…). Every value the door refuses fails each schema's parse; every value it accepts passes with the same window. Number-only members (a JSON body's canonical keys) refuse every string, as before. 494 cases plus a non-vacuity check.packages/spec/src/data/query-transport.test.ts: number and digit windows that are not a count are refused on every spelling (digits at the canonical path naming the spelling, one diagnostic);0stays a window.packages/spec/src/api/protocol.test.ts:HttpFindQueryParamsSchemarefuses-1,1.5, blank, whitespace,abc; keeps'0'and' 5 '.scripts/ablation-replace.mjs, run mode, restored by blob):limitmember back toQuerySchema's →query-transport.test.ts2 failed / 70 passed; the fold's string lowering back to "any finite number" → 4 failed / 68;HttpFindQueryParams.topback toz.coerce.number()→protocol.test.ts4 failed / 191. Each restored, blob equal to HEAD,git diff HEADempty.origin/mainatbda53bb0bmerged, after building the dependency closure of@objectstack/runtime,@objectstack/restand@objectstack/dogfood:pnpm --filter @objectstack/spec test: 709 files passed, 21545 tests passed, 1 todo (head82fb363ac).pnpm --filter @objectstack/metadata-protocol test: 259 files passed, 3 skipped; 21410 tests passed, 19 skipped.pnpm --filter @objectstack/rest test: 316 files, 6327 passed, 326 skipped.pnpm --filter @objectstack/runtime test: 435 files, 6885 passed, 19 skipped (an importer ofFindDataRequestSchema).typecheckexit 0 on@objectstack/spec,@objectstack/metadata-protocoland@objectstack/rest.(narrowing)arm), as CI runs it, under the verify lock: VERDICT command-exit 0, 289 files passed and 3 skipped (292), 2491 tests passed and 18 skipped (2509). The packages were built at685efa48and the tree it ran on was3fead31f3. The commits after it add theQueryPageWindowParsedtype alias (erased at run time), the dropped-refinements ledger header counts and regenerated JSON artifacts; none of them reaches a test at run time.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 114 families on this diff (the same 114 before and after merging main). All 114 ran, every final exit code is 0, recorded ascommand :: exit code, and--ranreconciled them: 114 run, 0 NOT-MEASURED, 0 UNRUN ("a DERIVED zero — all 114 recorded an exit code and none of them is 3").check-plugin-teardown-shape --self-test(the shallow clone lacked its pinned fixture commit, which was fetched),check:skill-examplesandcheck:dual-build-cjs-loads(exit 3: packages with nodist/in this container, which were built), andcheck:type-check-debt(a 600 s local timeout while its closure build waited for the verify lock). One was a real finding:check:spec-parsed-alias(QueryPageWindowSchemahad noParsedalias, added in3fead31f3).pnpm --filter @objectstack/spec check:generated: all 14 artifacts up to date on the final tree.check-changeset-no-majorandcheck-adr-0087-registrationexit 0 (not-required (no-migration-prescription)).Acceptance notes
QueryTransportParamsSchemaon its own still accepts any string on$top/$skip/skip: its digits are judged by the fold insideQueryWithTransportSchema(at the canonical path, naming the spelling, one diagnostic, the shapequery-transport.test.tsalready pins). No door parses the fragment alone.string(the refine is ledgered indropped-refinements.baseline.json); the number arm publishesinteger,minimum 0,maximum 9007199254740991.HttpFindQueryParamsSchemastill declares onlytop/skip, while the GET door also acceptslimit,offset,$top,$skip. Declaring those would widen the schema, not this card.expandwindows andengine.findwindows are not judged by the normalizer and stay as declared; a directengine.findwithoffset: -1still meets the driver (PR fix(metadata-protocol)!: the Data API refuses a malformed or negative page window with a 400 naming the parameter as written #25260's acceptance note 2).Generated by Claude Code