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. added
    pm:queueReady for the PM dispatch loop
    domain:specDomain lane — seat card indexed by label:pm:seat
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Oct 22, 2026
  2. 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

  3. 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

  4. 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

  5. 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

Labels

domain:specDomain lane — seat card indexed by label:pm:seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions