Skip to content

fix(spec)!: the published list-query schemas declare the page window the Data API enforces - #25300

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/project-thread-ovj591
Oct 22, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/project-thread-ovj591

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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.5 or a blank ?top= was valid. Now they declare what the door enforces, from one declaration.

  • One bound, exported by the spec (packages/spec/src/data/data-engine.zod.ts): QueryPageWindowSchema (z.number().int().nonnegative(), which in zod 4 is exactly Number.isSafeInteger(n) && n >= 0) and QueryPageWindowParamSchema (that number, or a non-blank string whose Number(...) is one, parsed to the count). Type aliases per ADR-0122.
  • The normalizer reads it (packages/metadata-protocol/src/protocol.ts, pageWindowValue): the window is judged by QueryPageWindowParamSchema.safeParse instead of a restated predicate. metadata-protocol already 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 (the POST /data/:object/query body, through QueryWithTransportSchema): the canonical limit / offset / top members 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 by QueryPageWindowParamSchema (it used to accept any finite number). QuerySchema itself is not touched (see Measured).
  • HttpFindQueryParamsSchema (the GET /data/:object querystring declaration): top / skip declare QueryPageWindowParamSchema instead of z.coerce.number(), which admitted -1, 1.5 and a blank value (as 0).
  • Generated artifacts: reference docs, api-surface, export-origins, declaration-map, json-schema.manifest, and two new rows in dropped-refinements.baseline.json (the string arm's refine has no JSON Schema form; its Number semantics 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 merging bda53bb0b)

The normalizer judges the top-level limit / offset of a findData call (pageWindowValue, two call sites). findData callers: GET /data/:object (querystring, no schema), POST /data/:object/query (validates FindDataRequestSchema first), the runtime dispatcher's twins (same schema on POST), declared object_operation endpoints, MCP query, action callData('query'), and server-built windows (import runner, connector pull, export loop: all integers).

Schema (card's list and found) Read by a door the normalizer judges? Disposition
FindDataRequestSchema → QueryWithTransportSchema / QueryTransportParamsSchema (limit, offset, top, $top, $skip, skip) yes, POST /data/:object/query and the dispatcher POST validate it narrowed
HttpFindQueryParamsSchema (top, skip) describes GET /data/:object (no runtime parse; the SDK reads it as the declaration) narrowed
EngineQueryOptionsSchema (data-engine.zod.ts:111/114/117) no: engine.find options; no door parses it, ObjectQL.find does not validate windows, and in-process callers reach it without the normalizer out of scope
QuerySchema (limit / offset / top) only through the transport schema above, where the members are overridden; QuerySchema is also the driver contract and the recursive expand value, whose windows the normalizer never judges not narrowed
get_record.limit (builtin-node-config.zod.ts:628) no: the executor calls the data service (ObjectQL, IDataEngine.find / findOne), not findData out of scope
ListRecordResponseSchema.pagination (contract.zod.ts:445/446) no: a response, not a request window out of scope
AuditMetaItemRequest / HistoryMetaItemRequest / ListNotificationsRequest limit (protocol.zod.ts:1823/1921/3281) no: metadata audit/history and notification routes, not the data list doors out of scope
DataEngineFindRequestSchema (RPC) no reader outside the spec out of scope

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 flow query window: get_record never reaches the normalizer, so its limit is not narrowed here.

Producer census (fractional or negative windows): examples/** on this tree, no hit; HotCRM at 4166f2146bec, 41 files name get_record, every literal limit is a positive integer (1, 500, 1000, and BILLING_HANDOFF_LINE_ITEM_LIMIT = 200), no fractional or negative window anywhere.

One door answer moves: the code, not the verdict

POST /data/:object/query validates its body against FindDataRequestSchema before findData, so now that the schema declares the window, a malformed one is refused there: 400 VALIDATION_FAILED with fields[0].field = query.limit / query.offset, where it was 400 INVALID_REQUEST from 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's POST twin moves the same way, by reading: it asks the same schema and answers requestSchemaRefusal, a VALIDATION_FAILED. GET /data/:object has no body schema and is byte-unchanged. Nothing newly refused, nothing newly accepted. packages/rest/src/data-page-window-door.test.ts's POST pin 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 real findData with 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_INTEGER and 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); 0 stays a window.
  • packages/spec/src/api/protocol.test.ts: HttpFindQueryParamsSchema refuses -1, 1.5, blank, whitespace, abc; keeps '0' and ' 5 '.
  • Ablations (scripts/ablation-replace.mjs, run mode, restored by blob): limit member back to QuerySchema's → query-transport.test.ts 2 failed / 70 passed; the fold's string lowering back to "any finite number" → 4 failed / 68; HttpFindQueryParams.top back to z.coerce.number() → protocol.test.ts 4 failed / 191. Each restored, blob equal to HEAD, git diff HEAD empty.
  • Full suites, on the tree with origin/main at bda53bb0b merged, after building the dependency closure of @objectstack/runtime, @objectstack/rest and @objectstack/dogfood:
    • pnpm --filter @objectstack/spec test: 709 files passed, 21545 tests passed, 1 todo (head 82fb363ac).
    • 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 of FindDataRequestSchema).
    • typecheck exit 0 on @objectstack/spec, @objectstack/metadata-protocol and @objectstack/rest.
    • Full dogfood (owed by the (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 at 685efa48 and the tree it ran on was 3fead31f3. The commits after it add the QueryPageWindowParsed type 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/objectstack derived 114 families on this diff (the same 114 before and after merging main). All 114 ran, every final exit code is 0, recorded as command :: exit code, and --ran reconciled them: 114 run, 0 NOT-MEASURED, 0 UNRUN ("a DERIVED zero — all 114 recorded an exit code and none of them is 3").
  • Five needed a second run. Four were local prerequisites: check-plugin-teardown-shape --self-test (the shallow clone lacked its pinned fixture commit, which was fetched), check:skill-examples and check:dual-build-cjs-loads (exit 3: packages with no dist/ in this container, which were built), and check: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 (QueryPageWindowSchema had no Parsed alias, added in 3fead31f3).
  • pnpm --filter @objectstack/spec check:generated: all 14 artifacts up to date on the final tree. check-changeset-no-major and check-adr-0087-registration exit 0 (not-required (no-migration-prescription)).

Acceptance notes

  • QueryTransportParamsSchema on its own still accepts any string on $top / $skip / skip: its digits are judged by the fold inside QueryWithTransportSchema (at the canonical path, naming the spelling, one diagnostic, the shape query-transport.test.ts already pins). No door parses the fragment alone.
  • The JSON Schema for the string arm stays string (the refine is ledgered in dropped-refinements.baseline.json); the number arm publishes integer, minimum 0, maximum 9007199254740991.
  • HttpFindQueryParamsSchema still declares only top / skip, while the GET door also accepts limit, offset, $top, $skip. Declaring those would widen the schema, not this card.
  • Nested expand windows and engine.find windows are not judged by the normalizer and stay as declared; a direct engine.find with offset: -1 still 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

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

12 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. ⚠️ 5 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/declaration-map/data.json, packages/spec/dropped-refinements.baseline.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 5 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/declaration-map/data.json, packages/spec/dropped-refinements.baseline.json, …) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 145 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 f47f7f222f1c8c03ab866efa428f4b50ff72b9b8 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d62cfdd4f607579163fa5dc59817007c2dbf8b1b — the merge of head 82fb363acae6bdb5cfb6dbaef380036f54e2647c into base f47f7f222f1c8c03ab866efa428f4b50ff72b9b8, 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 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 82fb363acae6bdb5cfb6dbaef380036f54e2647c
Local-runs: none
Scope: packages/spec/src/api/protocol.zod.ts, packages/spec/src/data/data-engine.zod.ts + packages/metadata-protocol/src/protocol.ts, packages/rest/src/crud-routes.ts, packages/runtime/src/domains/data.ts, packages/runtime/src/action-execution.ts, packages/services/service-automation/src/builtin/crud-nodes.ts, packages/spec/src/automation/builtin-node-config.zod.ts, packages/spec/src/data/query.zod.ts, packages/spec/src/api/contract.zod.ts, packages/spec/src/api/odata.zod.ts, packages/mcp/src/mcp-http-tools.ts, packages/metadata-protocol/src/protocol.page-window-declared.test.ts, packages/rest/src/data-page-window-door.test.ts, packages/spec/src/data/query-transport.test.ts, packages/spec/src/api/protocol.test.ts, packages/spec/dropped-refinements.baseline.json, .changeset/25269-page-window-declared.md

Inputs: card #25269 (body, claim 6349444847, os-dev-report 6351947108), PR #25300 (body, 16-file list, net diff against main), the check-runs on the head, and the cited code read at 82fb363a with git show. Nothing built, run or re-run. Check-runs read at 2026-10-22T22:07Z: every completed run is success or skipped; Lint & Repo Gates, TypeScript Type Check, Type Check · workspace, Build Core, Check Changeset, Governed Surface Queue Guard, Spec property liveness, Test Core (1/6), (3/6), (5/6) and (6/6) are success; Test Core (2/6) and (4/6) were still in_progress at that read and are reported as pending, not as passes. The dogfood battery does not run on a PR head (two speeds), so the dogfood evidence is the dev's cited run, judged in ③.

① Derived judgments

Each accept-set or public-surface change the diff implies, named right or wrong against the code at the head.

  1. New exports, additive. QueryPageWindowSchema (z.number().int().nonnegative()), QueryPageWindowParamSchema (that number, or a non-blank string whose Number(text) is one, transformed to the count) and the four ADR-0122 aliases (QueryPageWindow, QueryPageWindowParsed, QueryPageWindowParam, QueryPageWindowParamParsed) land in data-engine.zod.ts:927-957 and are recorded in api-surface/data.json, export-origins/data.json, declaration-map/data.json, json-schema.manifest/data.json. Right. check:spec-parsed-alias demanded the Parsed alias and the dev added it; every artifact gate is green on the head.

  2. QueryWithTransportSchema canonical limit / offset / top: any number → non-negative safe integer (describedPageWindowMember, data-engine.zod.ts:1347-1348, applied in the extend at :1376-1378). A door parses this: POST /data/:object/query validates FindDataRequestSchema before findData (crud-routes.ts:353-367) and the runtime dispatcher's POST twin does the same (domains/data.ts:242-243). Right — a narrowing of a published request schema a door enforces.

  3. QueryTransportParamsSchema $top / $skip / skip number arm: z.number() → QueryPageWindowSchema (:1023, :1026, :1059); the string arm stays z.string() at the member and is judged by the fold (lowerFoldedTransportValues, :1187-1194) through QueryPageWindowParamSchema, refused at the canonical path naming the spelling, one diagnostic — the shape query-transport.test.ts already pinned. Right. Residue, accepted as the dev declared it: QueryTransportParamsSchema parsed on its own still admits any string on those three keys. No door parses the fragment alone (FindDataRequestSchema.query reaches it only through QueryWithTransportSchema; the only other mentions in packages/spec/src are comments in odata.zod.ts and the FindDataRequestSchema docblock), and the JSON Schema string arm could not carry the rule either way (ledgered, item 9).

  4. HttpFindQueryParamsSchema.top / .skip: z.coerce.number() → QueryPageWindowParamSchema (protocol.zod.ts:2378-2379). Declaration-only on the wire: GET /data/:object hands req.query whole to findData (crud-routes.ts:160-165), the dispatcher's GET rides it verbatim (domains/data.ts:410), and no package parses this schema at run time (the client cites it as the declaration, client/src/index.ts:371). The TypeScript input type narrows too, from unknown (coerce) to number | string. Right — GET's runtime answer is byte-unchanged, the published declaration now matches it.

  5. The normalizer reads the spec's bound. pageWindowValue (protocol.ts:4787-4789) replaces the restated predicate (Number.isSafeInteger and non-negative, over a trimmed Number(text)) with QueryPageWindowParamSchema.safeParse(value); refusal code INVALID_REQUEST and text unchanged. metadata-protocol already imports from @objectstack/spec/data (protocol.ts:175-181), so no layering inversion. Right — same accept set, one declaration, both sides read it.

  6. Declared accept set equals the normalizer's, per parameter. Integer: zod 4 .int() is the safe-integer check on both sides. Minimum: 0 on both (0 stays a window; $top=0 asks for the total alone). Maximum: Number.MAX_SAFE_INTEGER on both; the JSON Schema number arm publishes integer, minimum 0, maximum 9007199254740991. String coercion: on both sides a non-blank trimmed string converted by Number, so ' 5 ', '1e2', '0x10' are counts on both and blank / whitespace / 'abc' / '-1' / '1.5' are refused on both. ?page=: a window on neither side — page sits in QUERY_PARAM_NEAR_MISS (protocol.ts:4573), a hint table whose header says nothing in it is accepted as input; ?page=2 is refused as an unknown parameter with a "did you mean the 'skip' query parameter" tail, so no schema should declare it and HttpFindQueryParamsSchema rightly does not. Right. The equality is pinned where the brief asks: protocol.page-window-declared.test.ts uses the real findData with a recording engine as the oracle (accepted iff the engine is reached, with the window it received), 13 schema rows × 17 numbers + 18 strings + 3 others, a non-vacuity check first, number-only members asserted to refuse every string as before.

  7. Scope measure — every schema left alone is out by evidence, not assertion. EngineQueryOptionsSchema: its one reader outside the spec is action-execution.ts:1997, Object.keys(shape) (a key set, no parse). QuerySchema (query.zod.ts:555-557): the driver contract and the recursive expand value, neither judged by the normalizer; its three members are overridden on QueryWithTransportSchema, so the door's declaration narrows without touching it. get_record.limit (builtin-node-config.zod.ts:628): the executor hands it to data.find / data.findOne (crud-nodes.ts:530-548), the IDataEngine, never findData. ListRecordResponseSchema.pagination (contract.zod.ts:441-448): a response. AuditMetaItemRequest / HistoryMetaItemRequest / ListNotificationsRequest limit (protocol.zod.ts:1823, :1921, :3284): metadata and notification routes whose own text says the server clamps, never refuses — not the data list doors. DataEngineFindRequestSchema: no reader outside the spec. ODataQuerySchema.$top / $skip (odata.zod.ts:186, :197, already .int().min(0)): its docblock records that it grades no runtime door. The MCP query tool (mcp-http-tools.ts:866-867) already declares int().positive().max(...) / int().nonnegative() and is not a published spec schema. findData callers (crud-routes.ts:165, :375; data-action-routes.ts; action-execution.ts:675; connector-pull.ts:343; import-runner.ts) are the two REST doors plus server-built or schema-less in-process windows. The dev's table is right in every row.

  8. One door answer moves, in its code only. On POST /data/:object/query and the dispatcher twin, a malformed window is now refused by the body schema: 400 VALIDATION_FAILED, fields[0].field = query.limit / query.offset (crud-routes.ts:361-366; requestSchemaRefusal, domains/data.ts:114-128), where it was 400 INVALID_REQUEST from the normalizer. Still 400, still before any read (data-page-window-door.test.ts pins zero driver reads on both doors). GET unchanged. Right as implemented; the rule reading is in ③.

  9. Dropped-refinements ledger. Two new sites — data/QueryPageWindowParam options[1].in and api/HttpFindQueryParams top.options[1].in — and the header counts move 240 → 242, 811 → 813. The string arm's rule is Number semantics, which PROJECTABLE_REFINEMENT_PATTERNS has no arm for and no pattern writes without approximating; build-schemas.ts holds the ledger equal to the generator and is green on the head. Right.

  10. Generated references (content/docs/references/**) regenerated, index.mdx counts 1542 → 1544. check:docs green. Right.

② Semver level

  • Clause-②: no (narrowing) — true. Nothing widens an accept set or a public surface; two published request schemas narrow, which clause2-line.mjs names as the arm's defining case (not a widening, but breaking). The fix(spec)!: title carries the same fact.
  • Metadata-protocol: none — true. The kind is the change to what a metadata author can write. No authorable metadata schema embeds QueryWithTransportSchema, QueryTransportParamsSchema, FindDataRequestSchema or HttpFindQueryParamsSchema (packages/spec/src at the head: protocol.zod.ts, the catalog string in plugin-rest-api.zod.ts:1020, and comments). QuerySchema is untouched. The card named the authored flow query window; the dev measured, and I confirm, that get_record.limit never reaches the normalizer (item 7), so it is not narrowed here, no authorable-surface/ baseline moves, and no flow migration row is owed by this PR.
  • patch on @objectstack/spec and @objectstack/metadata-protocol — true under the rule of record. pr-automation.yml WHICH LEVEL and AGENTS.md Post-Task Checklist 3: the level follows the metadata protocol alone, none takes patch, and a non-protocol breaking change ships patch and keeps its carriers. check-changeset-no-major.mjs (launch-window guard plus level axis) is green in Check Changeset. The metadata-protocol entry is a behaviour-preserving refactor (item 5).
  • Carriers present. The **BREAKING** banner, the before / now / remedy prose, and both declaration lines are in the changeset body; the PR body carries the same two lines.
  • ADR-0087 not-required (no-migration-prescription) — right. No metadata type, authorable key, object definition or stored row is added, removed, renamed or re-shaped; objectstack migrate meta has nothing to rewrite; the remedy line prescribes a request value, not a metadata rewrite; the vocabulary is ADR-0087's addendum and check-adr-0087-registration is green.

③ Boundary flags

Open 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 FindDataRequestSchema moves four POST body shapes (limit / offset negative or fractional) from the normalizer's 400 INVALID_REQUEST to the body schema's 400 VALIDATION_FAILED with fields[0].field naming query.limit / query.offset; status, timing (before any read) and GET are unchanged, and the REST pin was updated to say so. Rules: Prime Directive #10's corollary (never advertise what the runtime does not deliver; declared ≠ enforced is fixed, trimmed or filed, and the claim is kept as narrow as the enforcement, judged at every call site) and Prime Directive #12 (one strict internal contract, refused at the boundary) decide for A, with ADR-0049 as the recorded principle that a spec declaration must match what the runtime enforces. The card's own Wanted line asks that each schema a judged door reads declare the bound, and FindDataRequestSchema is the one schema a door actually parses (crud-routes.ts:359, domains/data.ts:242) — B keeps on it exactly the gap the card was filed for. ADR-0112 does not prefer one 400 catalog code over another here; it asks that a failing request answer a catalog code in the standard envelope, and VALIDATION_FAILED + fields[] is the envelope this door already answers for every other malformed member, {"limit":"ten"} included, so A removes a dialect rather than adding one. Recorded plainly for the seat: the acceptance line's literal "answer" reading is not met for the code and envelope of those four POST shapes; its parenthetical (the refusal already exists) is the invariant that holds. No escalation needed.

(narrowing) dogfood — met on the head's runtime, by the measured delta. The os-dev rule names the final head. The dev's run: tree 3fead31f3, packages built at 685efa48a, VERDICT exit 0, 289 files passed and 3 skipped, 2491 tests passed and 18 skipped. Measured at the head: 685efa48a..82fb363a touches four files — one export type QueryPageWindowParsed line (erased at run time), the ledger header counts in dropped-refinements.baseline.json, and one regenerated line each in api-surface/data.json and export-origins/data.json; 3fead31f3..82fb363a is those two JSON lines alone. Neither ledger has a runtime reader in packages/qa/dogfood/** or packages/spec/src/** (every mention is a comment). The built packages and the tree the suite ran against are runtime-identical to the head, and the reading is cited with its tree as AGENTS.md asks. Accepted; any further commit on the PR makes a new head that owes its own run.

Out-of-scope findings (os-dev-report 6351947108):

  1. get_record.limit is z.number(); its executor hands it to ObjectQL find / findOne, never findData. Card. It is a metadata-authoring trap under Prime Directive chore: version packages #10's filing test: the schema (builtin-node-config.zod.ts:628) admits a fraction or a negative, and both executor branches (crud-nodes.ts:530-548) need a positive integer — limit above 1 reaches the driver's LIMIT as written, so 2.5 meets the driver; anything else selects findOne. Rightly not in this PR: it is authored flow metadata, so its fix is Metadata-protocol: narrowed, minor, with a migration line — the opposite declaration from this PR's. Evidence for the card: the contract text above and that repro; no producer in examples/** or HotCRM 4166f2146bec, so low priority. Who acts: domain:spec with domain:automation.
  2. HttpFindQueryParamsSchema declares only top / skip while GET also takes limit / offset / $top / $skip; declaring them is a widening. No card. Not a defect, a contract violation or a trap: nothing the schema declares is false, every declared parameter is served, the extra spellings are published on the POST transport declaration, and GET /data/:object is excluded by name from the closed-set rule as an OPEN parameter set, so a "complete" GET declaration would not be a closed set either. It belongs in the PR's acceptance notes, where it is. If a consumer ever needs the GET declaration to carry every spelling, that is its own Clause-②: yes (widening) card with that consumer as evidence.
  3. Windows inside a nested expand query are judged by no one. No card from this PR's evidence. QuerySchema.expand values are nested QueryAST whose own description states that per-parent limit / offset / orderBy are NOT applied on the expand path, so the claim is already as narrow as the enforcement and there is no server bound to declare. If anything it is a liveness question about a declared key that is applied nowhere, for the Spec property liveness ledger's owner, not a window-bound narrowing for this lane.

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: Test Core (2/6) and (4/6); the queue requires their green independently of this record.

Implemented-by: claude/project-thread-ovj591
Reviewed-by: session_01B2EeooonSXrANpkpgUSfFZ

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:spec seat 2 (#18549) · os-bill · session session_01B2EeooonSXrANpkpgUSfFZ · 2026-10-22T22:14Z · head 82fb363acae6bdb5cfb6dbaef380036f54e2647c

  1. Shape: draft, base main, one closing keyword (Fixes #25269, after the session's attribution block). check-governed-merges.mjs --pr 25300 reads not governed and +415/−52, so this is an ordinary queue landing. Title fix(spec)!: carries the narrowing banner.
  2. Premise measured: the dev measured which published schemas describe a window the Data API normalizer (PR fix(metadata-protocol)!: the Data API refuses a malformed or negative page window with a 400 naming the parameter as written #25260) judges: only FindDataRequestSchema (through QueryWithTransportSchema / QueryTransportParamsSchema) and HttpFindQueryParamsSchema. The review confirmed every row of that scope table by evidence. The bounds have one source: the spec exports QueryPageWindowSchema / QueryPageWindowParamSchema, and the normalizer parses through them.
  3. Gates: check-runs on this head 36 success and 6 path- or opt-in-skipped, with none failed or pending. The dev's local dispatch-gates run: 114 derived, 114 run with final exit 0, 0 NOT-MEASURED. spec check:generated: 14 of 14 fresh. Dogfood (the (narrowing) arm): 289 files / 2491 tests green on tree 3fead31f3; the review measured that the only later changes are one erased type alias and two regenerated JSON lines with no runtime reader, so no re-run is owed on this head.
  4. Contract review: PASS at CONTRACT_REVIEW_TIER on this head, record 6352380127.
  5. Accepted deviations:
    • The branch is the card thread's claude/project-thread-ovj591, as claim 6349444847 names it.
    • The dev's open question, answered A by the review on the rules (Prime Directives chore: version packages #10 and Add comprehensive test suite for Zod schema validation #12, ADR-0049), no escalation: a bad POST /data/:object/query window now gets the body schema's 400 VALIDATION_FAILED with fields[0].field = query.limit instead of the normalizer's 400 INVALID_REQUEST. It is still 400 and still refused before any read; GET is unchanged; the envelope is the one {"limit":"ten"} already gets. The card's "no door's runtime answer changes" holds for status and timing, not for the code of those four POST body shapes. The seat accepts that: B would have kept the POST contract declaring what the door refuses.
    • QueryTransportParamsSchema parsed alone still admits any string on $top / $skip / skip. No door parses the fragment alone; recorded in the PR's acceptance notes.
    • origin/main was not merged before landing (10 commits behind, none in these files); the merge queue rebuilds on main.
  6. Prose faces (the seat's own read):
    • the changeset (patch for spec and metadata-protocol, Clause-②: no (narrowing), Metadata-protocol: none, ADR-0087 not-required (no-migration-prescription)), all judged true by the review: no authorable schema embeds the four narrowed request schemas, so no flow migration row is owed;
    • the regenerated protocol.mdx and data-engine.mdx references.
  7. Out of scope:
  8. Landing: pr_ready + automerge_enable through the relay now. The seat follows it to MERGED.

Generated by Claude Code

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

Development

Successfully merging this pull request may close these issues.

spec: the published query schemas declare limit / offset / top / skip as plain numbers the Data API now refuses (declared ≠ enforced)

2 participants