Skip to content

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

Closed

Description

@objectstack-fleet

Filing gate ③: an out-of-scope finding the domain:engine seat disposed in its ACCEPT of PR #25260 (comment 6347650135, item 5), confirmed by that PR's contract review (6347561377). It does not block that PR, which merged as 17f3c65a1 (card #25229).

  • Who acts: domain:spec.
  • Dedupe: none found.

What happens on main

  • Since fix(metadata-protocol)!: the Data API refuses a malformed or negative page window with a 400 naming the parameter as written #25260, every Data API list door reaches one window normalizer: a fractional, negative or malformed limit / offset / $top / $skip / ?page= answers 400 naming the parameter as written.
  • The published schemas still declare the window as any number, so a client generated from them, or an author validating against them, is told a value is valid that the server refuses. Examples on origin/main at 17f3c65a1:
    • packages/spec/src/data/data-engine.zod.ts:111 / :114 / :117 (limit, offset, top: z.number().optional()); the same file's :186 / :188 already say .int().min(1) / .int().min(0).
    • packages/spec/src/api/protocol.zod.ts:2375 / :2376 (top, skip: z.coerce.number().optional()), and :1823, :1921, :3281 (limit).
    • packages/spec/src/api/contract.zod.ts:445 / :446.
    • packages/spec/src/automation/builtin-node-config.zod.ts:628 (a flow query's limit: an authored fractional or negative window is now refused at run time).
  • Not every hit is a Data API window (analytics.zod.ts:1064 already says .int().nonnegative()); the card measures which schemas the normalizer's doors actually read.

Wanted

  • Each schema that describes a window the Data API normalizer judges declares what the server enforces: limit / top .int() at least 0 (or 1, as the normalizer says), offset / skip .int().nonnegative(); the same accept set on both sides, one source for the bounds if the spec can export it.
  • A narrowing of a published schema: Clause-② and Metadata-protocol declared per AGENTS.md (an authored flow query window is metadata), the changeset level and ADR-0087 disposition they imply, and a migration line for the authored-flow case.

Acceptance

  • For every listed schema, a value the server refuses fails the schema's parse and a value it accepts passes (a table test over the normalizer's own cases).
  • No door's runtime answer changes (the server already refuses these).

Activity

  1. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 11
    Session: session_0153NL7xGym5Dpyb4sQbsArM (the card thread; seat session session_01B2EeooonSXrANpkpgUSfFZ posts this claim)
    Account: os-bill
    Branch: claude/project-thread-ovj591 (the card thread's designated push branch; claude/issue-N-slug is the convention, and this is the measured platform exception the pm-dispatch runbook records)
    Worktree: objectstack-issue-25269
    Domain: domain:spec
    Seat: domain:spec#2 (seat post #18549)
    File surface: packages/spec/src/data/data-engine.zod.ts, packages/spec/src/api/protocol.zod.ts, packages/spec/src/api/contract.zod.ts, packages/spec/src/automation/builtin-node-config.zod.ts (the window fields the Data API normalizer judges, as measured), a shared bounds export if the layering allows it, a migration entry for the authored flow query window, their regenerated references, tests and one changeset (stop on breach; explain in the report)
    Container & model: M, mode:thread, model: default (dispatch-gates --tier: no path-derived mandate), effort: high; a published-schema narrowing owes one CONTRACT_REVIEW_TIER review of record before the queue
    Clause-②: no (narrowing, expected; the dev measures it from the diff)
    Responsibility: n/a — not a defect in a door; the published schemas declare a wider accept set than the Data API normalizer (PR #25260) enforces
    Thread-read: none
    Serial constraints cleared: none. No open PR touches the four schema files (open-PR file scan).


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 25269,
      "status": "done",
      "branch": "claude/project-thread-ovj591",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/25300",
      "session": "session_0153NL7xGym5Dpyb4sQbsArM",
      "premise_still_valid": true,
      "summary": "The spec exports one page-window bound, QueryPageWindowSchema (z.number().int().nonnegative(), exactly Number.isSafeInteger(n) && n >= 0) and QueryPageWindowParamSchema (that number or a non-blank string whose Number() is one), and the findData normalizer (pageWindowValue) now judges every window by it. FindDataRequestSchema (through QueryWithTransportSchema: limit/offset/top members, and the $top/$skip/skip number arm plus the fold's string lowering) and HttpFindQueryParamsSchema (top/skip, was z.coerce.number()) declare that bound. Measured scope: only those two request schemas describe a window the normalizer judges; EngineQueryOptionsSchema, QuerySchema itself (driver contract and recursive expand), get_record.limit (its executor calls ObjectQL find/findOne, never findData), the response pagination in contract.zod.ts and the audit/history/notification limits are not judged and are left as declared. Census: no fractional or negative window producer in examples/** or HotCRM 4166f2146bec. Clause-②: no (narrowing), Metadata-protocol: none (request schemas, no authored metadata), patch on spec + metadata-protocol, ADR-0087 not-required (no-migration-prescription); no authored-flow migration line is owed because get_record never reaches the normalizer.",
      "tests": "New packages/metadata-protocol/src/protocol.page-window-declared.test.ts: oracle = the real findData with a recording engine; 13 schema rows x (17 numbers + 18 strings + 3 others) = 494 cases + 1 non-vacuity check; with protocol.page-window.test.ts 520/520 passed. spec: query-transport.test.ts + protocol.test.ts 267/267. rest data-page-window-door.test.ts 4/4 (POST pin updated to 400 VALIDATION_FAILED, fields[0].field query.KEY, zero reads). Full suites after merging origin/main bda53bb0b: spec 709 files / 21545 passed, 1 todo (head 82fb363ac); metadata-protocol 259 files passed, 3 skipped / 21410 passed, 19 skipped; rest 316 files / 6327 passed, 326 skipped; runtime 435 files / 6885 passed, 19 skipped; typecheck exit 0 on spec, metadata-protocol, rest. Ablations via scripts/ablation-replace.mjs (spec tests resolve src, no build needed), each restored with blob equal to HEAD and git diff HEAD empty: limit member reverted to QuerySchema's -> query-transport 2 failed / 70 passed; string lowering reverted to any finite number -> 4 failed / 68; HttpFindQueryParams.top reverted to z.coerce.number() -> protocol.test 4 failed / 191.",
      "gates": "dispatch-gates --commands --repo objectstack-ai/objectstack: 114 families (same list before and after merging main). All 114 ran with final exit 0, recorded as command :: exit code; --ran: 114 derived, 114 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero). Second runs: check-plugin-teardown-shape --self-test (shallow clone lacked pinned fixture 621a4876, fetched -> 0), check:skill-examples and check:dual-build-cjs-loads (exit 3, packages without dist/ in this container, built -> 0), check:type-check-debt (local 600 s timeout waiting on the verify lock -> 0), check:spec-parsed-alias (real finding: QueryPageWindowParsed alias added in 3fead31f3 -> 0). spec check:generated: all 14 artifacts up to date on the final tree. check-changeset-no-major exit 0, check-adr-0087-registration exit 0. CI check-runs on 82fb363ac: in_progress at report time (not waited for, per os-dev).",
      "dogfood_battery": "tree 3fead31f3 (packages built at 685efa48) · os-verify-lock: VERDICT command-exit 0 · Test Files 289 passed, 3 skipped (292) · Tests 2491 passed, 18 skipped (2509) · no failures. Final head 82fb363ac differs only by regenerated JSON artifacts (api-surface, export-origins) and the commits before it by a type-only alias and the dropped-refinements ledger header counts: nothing a test reaches at run time.",
      "mcp_calls": "0 GitHub MCP calls (one non-GitHub harness call, subscribe_pr_activity on #25300)",
      "api_writes": "2 — relay pr_create (draft #25300 with assignee os-bill, request fw-20261022T213954Z-f8f72f, read-back identical 10897 bytes); this os-dev-report comment on #25269 via scripts/pm/post-stamped.mjs. git push is not a REST write.",
      "open_questions": [
        {
          "question": "The card's acceptance says no door's runtime answer changes. Declaring the window on FindDataRequestSchema moves one answer's CODE: POST /data/:object/query (and the runtime dispatcher's POST twin) now refuses a malformed window at the body schema with 400 VALIDATION_FAILED (fields[0].field query.limit / query.offset) instead of 400 INVALID_REQUEST from the normalizer. Still 400, still before any read, the same answer the door already gave {limit: 'ten'}; GET is unchanged. Accept this reading?",
          "options": [
            "A: accept (as implemented): the POST door's published schema declares what it enforces; the refusal code follows the schema door, matching every other malformed body",
            "B: leave FindDataRequestSchema wide and narrow only HttpFindQueryParamsSchema: no answer moves, but the POST contract stays declared-not-enforced, the card's main subject"
          ],
          "recommendation": "A, because the card's own parenthetical ('the server already refuses these') is about the verdict, which does not change; B keeps exactly the declared != enforced gap the card was filed for, and a generated client would still be told -1 is valid."
        }
      ],
      "out_of_scope_findings": [
        "noted, not filed (no class a/b/c): the flow get_record node's limit (builtin-node-config.zod.ts) is z.number() and its executor hands it straight to ObjectQL find; a fractional limit above 1 reaches the driver, a limit at or below 1 selects findOne. Not judged by the findData normalizer, so outside this card; no producer of a fractional or negative value found (examples/**, HotCRM 4166f2146bec). carrier: none. Dedupe words: get_record limit integer, flow node window, engine.find limit validation",
        "noted, not filed: HttpFindQueryParamsSchema declares only top/skip while GET /data/:object also accepts limit, offset, $top, $skip; declaring them is a widening of the published schema. carrier: none. Dedupe words: HttpFindQueryParams missing limit offset, GET list querystring declaration",
        "noted, not filed: windows inside a nested expand query (QuerySchema recursion) are not judged by any normalizer and stay any number. carrier: none. Dedupe words: expand nested limit, nested query window"
      ]
    }

    Generated by Claude Code

  3. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT → PR #25300 comment 6352490708 · contract review PASS 6352380127 · head 82fb363aca · domain:spec#2 · session session_01B2EeooonSXrANpkpgUSfFZ · follow-up #25302


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Landing record · domain:spec seat 2 · session session_01B2EeooonSXrANpkpgUSfFZ · 2026-10-22T22:51Z

    Probe Reading
    PR #25300 merged: true, merge commit 5f30f00bf8cd462e707d0349479f863ab24a0aeb
    origin/main carries 5f30f00bf8 (git merge-base --is-ancestor)
    Queue branch no gh-readonly-queue/main/*25300* ref remains
    Card closed completed by Fixes #25269; pm:dispatched removed in this act

    ACCEPT 6352490708 · contract review PASS 6352380127. Follow-up: #25302.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions