Skip to content

[finding] GET /data/import/jobs never refuses its declared limit bound (min(1).max(200)): ?limit=0 silently answers the default 50 rows and ?limit=500 answers 200 #20061

Closed

Description

@objectstack-fleet

Filing gate: ① a product defect with a named landing point and a repro. Found by the os-dev round of #19567 (PR #20060) as an out-of-scope finding. The domain:cli execution seat #6024 re-read both sites on origin/main 3557f85f before filing. ⛔ Filed bare: routing and grading are triage's. ⛔ Not a claim.

Declared vs enforced

leg reading on origin/main 3557f85f
declares packages/spec/src/api/export.zod.ts:503, ListImportJobsRequestSchema: limit: z.number().int().min(1).max(200).default(50)
enforces packages/rest/src/rest-server.ts:9714: Math.min(200, Math.max(1, Number(q.limit) || 50)) refuses nothing
?limit=0 0 || 50 gives 50: a silent substitution of the default window for a value the declaration refuses
?limit=500 clamped to 200, not refused
?limit=abc NaN || 50 gives 50

Why it matters now: since PR #20060 (card #19567), the SDK's data.listImportJobs sends limit exactly as the caller wrote it and relies on the declared door to refuse. This door does not refuse, so for this one method a caller's 0 still becomes 50 rows.

What this card asks

Make the door refuse a limit outside its declaration with 400 VALIDATION_FAILED (the parseIntegerParam shape the parsed doors use), and pin code and status with a lit control on a conforming value. offset on the same line (Math.max(0, Number(q.offset) || 0)) has the same shape. Whether it belongs in the same round is for the taker to measure against its own declaration.

Activity

  1. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: an API a customer can call | api-backend.query-contract-matrix | P2

    Triage: first grade — bug · priority:p2 · domain:cli · area:api · pm:queue

    Triage: lands in packages/rest (rest-server.ts:9714 on origin/main a8bcce6, Math.min(200, Math.max(1, Number(q.limit) || 50))) ⇒ domain:cli; rationale: a declared bound not held — ListImportJobsRequestSchema (packages/spec/src/api/export.zod.ts:500) declares limit min(1).max(200), and the door substitutes or clamps instead of refusing, so ?limit=0 answers 50 rows; since PR #20060 the SDK forwards limit as written and relies on this door to refuse. Runs but answers wrong ⇒ p2.

    Triage seat #6015 · session_01CRZSc7dU8oDStbTbSwhuZe · 2026-09-25T04:42Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card (no comments yet) and origin/main. Dedupe by this seat over 1,113 cards (all open, plus closed since 2026-09-18): Number(q.limit) → 2 hits (this card, #20062), listImportJobs → 1 (this card).

    Execution notes

    1. Same batch as [finding] three REST doors read ?limit= with a bare Number(), so a non-numeric limit answers 200 with a widened or substituted window: search loses its cap, export returns one row, meta history returns the whole log #20062: same mechanism (a bare Number() read where the parsed doors use parseIntegerParam, packages/runtime/src/query-param.ts:158), same file. They stay two cards because the doors and symptoms differ; one claim may take both.
    2. Refuse outside the declaration with 400 VALIDATION_FAILED, and pin code and status with a lit control on a conforming value. offset on the same line is the taker's to measure against its own declaration.
  2. added
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    bugSomething isn't working
    and removed on Sep 25, 2026
  3. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 10 (serial; one claim takes #20061 and #20062, per triage's execution note on both)
    Session: session_01TnPAC1UsTGfHPXVUCL6iLn
    Account: os-steve
    Branch: claude/issue-20061-rest-limit-parsing
    Worktree: objectstack-issue-20061
    Domain: domain:cli
    Seat: domain:cli#1
    File surface: packages/rest/src/rest-server.ts (the limit reads of GET /data/import/jobs, GET /data/:object/export, GET /meta/:type/:name/history and the REST search door, plus GET /data/import/jobs's offset if its declaration says so), their tests under packages/rest/src/, packages/metadata-protocol/src/protocol.ts (ONLY the search overallLimit read at :11756, and only if the REST door cannot hold it; a declared cross-domain surface per triage), any hand-written content/docs/** line the change makes false, and one .changeset/20061-*.md. packages/spec/** and packages/runtime/** are read-only. CHANGELOG.md and package.json files belong to the Version Packages PR. Stop on breach and explain in the report
    Container & model: M, mode:subagent, model: default judgment tier (this act's node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/rest/src/rest-server.ts answered: no path-derived mandate). A family of published REST doors that must start refusing what their declarations already refuse, a narrowing that ships BREAKING, so it gets the default judgment tier.
    Clause-②: no (narrowing)
    Thread-read: 5826869372
    Serial constraints cleared: the maintainer's order in this seat's session (「继续派发」) moves the lane on once a card's PR is in the merge queue: R9's PR #20128 (#19790) is in the merge queue (its timeline carries added_to_merge_queue), and it touches packages/rest/src/rest-server.ts in the nav filter and the docs reads, not these doors. The branch starts from origin/main and merges main after #20128 lands if the two overlap. The file lists of all 11 open PRs were read in this act. The only other one touching these files is the draft PR #20125 (#20102, the saved-report retirement, another lane): its rest-server.ts hunks sit at :1634-1716, :4420 and :12642-12988, and its protocol.ts hunks at :3382-9732. None overlaps these doors (:7710, :9714, :9841, the search route, protocol.ts :11756), so whichever lands second merges main. The Version Packages PR #17076 owns the CHANGELOG and package.json versions. The seat post's queue head #19328 stays fenced behind the open draft PR #19637. The NORTH-STAR 优先级 3 pre-pick read at 6780e34a: the domain:cli pm:queue holds no open P0/P1

    Taken 2026-09-25T07:11Z, origin/main 6780e34a. The same claim is posted on #20062. Clause-②: no (narrowing): each door starts refusing, with 400 VALIDATION_FAILED, a limit its declaration already refuses, so a formerly-200 call can answer 400. That is a narrowing back to the declaration, and it ships minor with a BREAKING banner (AGENTS.md Post-Task Checklist 3; the #19576 and #19567 rounds). A door found to WIDEN anything stops the dev. The dev posts no second claim; this comment is its identity on both cards.

  4. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
    "issue": 20061,
    "also_closes": 20062,
    "status": "done",
    "branch": "claude/issue-20061-rest-limit-parsing",
    "pr": "#20137",
    "session": "session_01TnPAC1UsTGfHPXVUCL6iLn (harness-stamped Claude-Session id; subagent inherits the PM seat session)",
    "premise_still_valid": true,
    "summary": "Four published REST doors now read ?limit= against their own declaration and refuse a value outside it with 400 VALIDATION_FAILED + fields[] (fields[0].field = the parameter, fields[0].code = ADR-0114 invalid_type / min_value / max_value), where they used to substitute, clamp or drop it and answer 200: GET /data/import/jobs (limit AND offset, through ListImportJobsRequestSchema: min(1).max(200) / min(0)), GET /meta/:type/:name/history (through HistoryMetaItemRequestSchema.limit, z.number(): finite numbers incl. 1.5/0 still forwarded), GET /data/:object/export and GET /search (no declared request schema: a whole number; their own floor/cap/clamp untouched). One private reader readDeclaredQueryNumber in packages/rest/src/rest-server.ts (no new export; published rest .d.ts byte-identical to merge-base bc80e16) throws validationFailure (@objectstack/types) into each door's existing handleRouteError / mapDataError catch, so rest-server.ts gains no response write site. PM assumptions: (1) the parseIntegerParam cycle is CONFIRMED (runtime depends on rest), nothing moved; (2) the declared-schema idiom holds, but the measured fields[].code for an unreadable number is invalid_type (zodIssuesToFields), not runtime's invalid_number: open question below; (3) export serves NO declared schema (CreateExportJobRequestSchema is the unmounted POST contract; ExportRequestSchema is unbound) and searchAll has no production caller but the REST door, so protocol.ts is untouched; (4) PARTLY FALSIFIED: an empty ?limit= answered the default only on import jobs and search; export answered a one-row export and history zero events (Number('') = 0), so empty stays absent on the first two and is refused on the other two (reading it as absent there would have widened the window to 10000 rows / the whole log). No door accepts anything it refused before; no conforming value's answer changes. Merged origin/main (no rebase) after PR #20128 landed on rest-server.ts; no conflict.",
    "tests": "All at final head 74898ed unless noted. NEW packages/rest/src/rest-server-limit-param-parsing.test.ts (43 cases: per door, refusal pins status 400 + code VALIDATION_FAILED + fields[0].field + fields[0].code + service never called; lit controls pin the argument the service received, including the unchanged ranges). Before the fix (6780e34): "Tests 24 failed | 19 passed (43)" (every refusal row answered 200). After: "Tests 43 passed (43)". FULL rest suite: pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 → "Test Files 196 passed (196)" / "Tests 3327 passed | 1 skipped (3328)" (os-verify-lock VERDICT command-exit 0). pnpm --filter @objectstack/rest run typecheck → exit 0 (tsc --noEmit + "check:test-typecheck: OK ... 0 file(s) / 0 error(s)"). The first full run at b742252 had 2 reds, both caused by this diff and resolved in 3d07f06: the canonical query-slot census read the reader's parameter named query as a query slot (renamed queryParams), and the search closed-set loop in rest-server-closed-query-params.test.ts sent limit x expecting 200 (fixture now sends a valid value per name, as its export twin already did). ABLATION via scripts/ablation-replace.mjs WRAP mode after committing the fix (vitest reads src/ through the relative import, so no dist leg): (a) import-jobs limit read restored to Math.min(200, Math.max(1, Number(q.limit) || 50)): "anchor x1 → x0", blob 02ceccd1 → cab07300, "Tests 8 failed | 35 passed (43)" = exactly the 8 import-jobs limit refusal rows; (b) search limit read restored to req.query?.limit ? Number(req.query.limit) : undefined: blob 02ceccd1 → 9b589c00, "Tests 4 failed | 39 passed" = exactly the 4 search refusal rows; both restores "ok restored: blob == HEAD (02ceccd1373f) and git diff HEAD is empty". PUBLIC SURFACE: rest dist/index.d.ts and dist/index.d.cts built from merge-base bc80e16 rest-server.ts vs HEAD: cmp IDENTICAL (restore proven by blob hash e2da6fba == HEAD). pnpm lint (full union, eslint . --no-inline-config) at 74898ed: exit 0 in 110s.",
    "gates": [
    "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0 :: ✓ check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.",
    "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0 :: ✓ check-adr-0087-registration --self-test: 441 assertions over real temp git repos (real scan()/assertInputs() path)",
    "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0 :: ✓ This diff introduces no major bump.",
    "node scripts/check-changeset-no-major.mjs --self-test :: exit 0 :: ✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #",
    "node scripts/check-ci-filter-parity.mjs :: exit 0 :: OK: all 184 declared cross-package glob(s) (131 unique) are covered by core or crosspkg, every crosspkg entry still covers one, and the test job's if: still nam",
    "node scripts/check-closing-keyword-parity.mjs :: exit 0 :: check-closing-keyword-parity: OK (3 parsers agree on all 9 keywords and both measured separators; sweep found 5 file(s) carrying the grammar across 9555 tracked file(s), ",
    "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0 :: ✓ check-closing-keyword-parity --self-test: 40 assertions, 5 mutations of the shipped parsers each driven to red.",
    "node scripts/check-comment-mask-adoption.mjs :: exit 0 :: OK check:comment-mask-adoption — 14 private comment-stripper(s) under packages/** + examples/, all 14 recorded and every recorded row still reached (13 unconverted, 1 s",
    "node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0 :: PASS check-comment-mask-adoption --self-test (0 failure(s))",
    "node scripts/check-comment-mask-corpus.mjs :: exit 0 :: ✓ comment-mask corpus sweep [scripts/js-comment-mask.mjs]: 7163 files, 0 disagree, 0 unparseable, 78.1s (comparator self-test: 26 cases pass).",
    "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0 :: ✓ No changeset from the merge base modified or deleted by this diff (#17712).",
    "node scripts/check-empty-changeset.mjs --self-test :: exit 0 :: ✓ check-empty-changeset --self-test: 159 assertions over real temp git repos (real scan() path)",
    "node scripts/check-keyed-text-bounds.mjs :: exit 0 :: ✓ check:keyed-text-bounds: 112 .object.ts files under packages/
    + apps/
    * + examples/** (walk is repo-wide; 0 outside), 117 object declarations, 250 declared index ent",
    "node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0 :: PASS check-keyed-text-bounds --self-test (0 failure(s))",
    "node scripts/check-platform-object-tenancy-census.mjs :: exit 0 :: ✓ platform-object tenancy census matches the tree: 84 platform-namespace objects, 58 in the machinery's reach, 26 outside it, every exclusion explained by a declaration o",
    "node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0 :: ✓ check-platform-object-tenancy-census self-test: all checks pass (84 objects, 26 outside the machinery)",
    "node scripts/check-plugin-teardown-shape.mjs :: exit 0 :: ✓ check:plugin-teardown-shape: 69 Plugin implementation(s) across 6594 source(s) under packages/; every teardown-shaped method (stop / shutdown / close / dispose) sits ",
    "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0 :: ✓ check-plugin-teardown-shape self-test: 48 cases pass (real pre-#10375 fixture reds, the repaired file and both delegating-alias directions stay green, every roster name",
    "node scripts/check-registry-log-declared.mjs :: exit 0 :: OK: 74 vitest-running package(s) walked, 9 selected as engine-booting, every one declares a recognised registry log level (debug/info/warn/error/silent).",
    "node scripts/check-registry-log-declared.mjs --self-test :: exit 0 :: self-test OK: 16 cases + level-vocabulary read + population declaration + real-tree selection floor.",
    "node scripts/check-rest-log-spy-declared.mjs :: exit 0 :: OK: 30 of 198 test file(s) beside packages/rest/src/log.ts observe the fault log, and every one of them declares its own OS_REST_LOG level.",
    "node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0 :: check-rest-log-spy-declared self-test reached its verdict: 23 case(s), 0 failure(s).",
    "node scripts/check-system-context-census.mjs :: exit 0 :: check-system-context-census: OK — 112 elevation read sites in 20 packages across 45 files, living in 93 symbol(s); the page cites 106 symbol(s) against 106 required, over",
    "node scripts/check-system-context-census.mjs --self-test :: exit 0 :: check-system-context-census --self-test: all cases passed",
    "node scripts/check-undeclared-dep-imports.mjs :: exit 0 :: ✓ check:undeclared-dep-imports: 81 workspace packages under packages/
    + apps/** + examples/, 2592 non-test src files, 2108 @objectstack/* specifiers (0 assembled, not",
    "node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0 :: PASS check-undeclared-dep-imports --self-test (0 failure(s))",
    "node scripts/docs-audit/check-affected-docs.mjs :: exit 0 :: ✓ affected-docs self-test: 605 cases pass.",
    "node scripts/docs-audit/check-drift-comment.mjs :: exit 0 :: ✓ check-drift-comment: 66 cases pass across 5 fixture diff(s).",
    "node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0 :: ✓ self-test passed",
    "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0 :: ✓ check:duration-unit-keys — 204 unit-declaring numeric key(s) across 2589 source file(s) all carry their unit in the key name (or in a sibling unit, or under a declare",
    "pnpm check:authz-resolver :: exit 0 :: ✓ check:authz-resolver: single shared authorization resolver intact; both entry points delegate.",
    "pnpm check:changeset-gate-self-tests :: exit 0 :: ✓ check-changeset-no-major --self-test: 339 assertions (frontmatter dialects measured against @changesets/parse + the pre/exit exemption switch in both directions + the #",
    "pnpm check:cross-package-test-inputs :: exit 0 :: OK: 29 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob (6 of them on a split "test:repo" task); 13 walked root(s) judged, 3 on",
    "pnpm check:dispatcher-error-vocabulary :: exit 0 :: check-dispatcher-error-vocabulary: OK — 52 unregistered code-stamping site(s), all classified; 2 awaiting a ledger entry (#8846).",
    "pnpm check:doc-authoring :: exit 0 :: ✓ doc authoring guard: sibling-package prose ids hold the baseline — 815 pinned site(s) across 231 file(s), 92023 string(s) read in 1253 parsed source(s), no growth, no b",
    "pnpm check:driver-memory-census :: exit 0 :: check-driver-memory-census: OK — every declaration is ledgered, every ledger entry is live, and every ruled file states "#6664 census: 2 ruled consumers". This gate polic",
    "pnpm check:dts-closure :: exit 0 :: check-dts-closure self-test: all assertions passed.",
    "pnpm check:dual-build-cjs-loads :: exit 3 :: PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/.",
    "pnpm check:engine-double-contract :: exit 0 :: check-engine-double-contract: OK — 902 pinned, 133 in the DEBT ledger, 3 exempt.",
    "pnpm check:gitlink-declared :: exit 0 :: check-gitlink-declared: OK (9555 index entries -- 0 gitlink(s) at mode 160000; no .gitmodules in the index, so nothing is declared; nothing to declare).",
    "pnpm check:issue-citations :: exit 0 :: ✅ check-issue-citations --self-test: grammar narrowed, four 404 causes kept apart, both board strategies agree, diff scope red AND green, scope contract pinned (73 cases,",
    "pnpm check:lean-entry-closure :: exit 0 :: Admitted set held exactly (15 packages); 6 denied names absent.",
    "pnpm check:logger-receiver-detach :: exit 0 :: OK every log channel keeps its receiver: 2860 non-test TS file(s) walked, 0 detach(es) on the 5 declared receiver-sensitive sink spelling(s).",
    "pnpm check:nul-bytes :: exit 0 :: check-nul-bytes: OK (scanned 9548 text file(s) -- 9548 tracked, 0 untracked-not-ignored; skipped 7 binary; no raw ASCII control bytes).",
    "pnpm check:objectql-double-limit :: exit 0 :: OK ObjectQL double limit conformance holds: 410 double(s) graded, 212 apply the caller's bound or refuse it loudly.",
    "pnpm check:objectui-changeset :: exit 0 :: ✓ objectui-range --self-test: all checks passed",
    "pnpm check:org-identifier :: exit 0 :: check-org-identifier: OK (2882 author-facing source file(s), 17 session binding(s) resolved, no removed session.tenantId alias).",
    "pnpm check:page-declaration-shape :: exit 0 :: check-page-declaration-shape: OK — 34 page entries across 2872 sources under packages/
    , examples/, apps/ all reach the kernel through a discoverable declaration (:", "pnpm check:pm-changeset-deadline-census :: exit 0 :: ✓ changeset-deadline-census --self-test: all cases passed across 5 batteries (what counts as a named target, the controls that make a zero a reading, the verdict truth ta", "pnpm check:published-files :: exit 0 :: ✓ check:published-files — 70 publishable package(s) of 81 workspace member(s) declare a fileswhitelist that covers every entry point plus CHANGELOG.md and admits no te", "pnpm check:query-options-erasure :: exit 0 :: ✓ query-options-erasure ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new, and every file measured parsed. Every other non-test file under packages/ is c", "pnpm check:refd-timer-probe :: exit 0 :: OK check-refd-timer-probe: 7158 source file(s) swept; the process-global timer probe is read in packages/qa/refd-timer-testkit/src/index.ts and nowhere else.", "pnpm check:route-envelope :: exit 0 :: ✓ Express-style response modules — 4 module(s) discovered and audited (walked, not enumerated — #9937), 12 hand-built body/bodies (count reported, NOT pinned): 2 conforma", "pnpm check:slot-lookup :: exit 0 :: ✓ slot-lookup ratchet holds: 106 unswept site(s) in 25 file(s), none new, and every file in the population parsed. Every other file under packages/ is covered bypnpm li",
    "pnpm check:sourcemap-no-sources-content :: exit 0 :: check-sourcemap-no-sources-content self-test: all assertions passed.",
    "pnpm check:test-source-alias :: exit 0 :: check-test-source-alias OK — 74 packages with tests scanned; 61 registered as still resolving a workspace dep through dist/; 50 published subpath(s) resolved through ev",
    "pnpm check:tier-file-adoption :: exit 0 :: OK: 81 workspace package(s) walked, 70 nightly-tier test file(s) on disk (70 e2e, 0 live), owned by 1 package(s); every one reads OS_TEST_TIERS.",
    "pnpm check:type-check-coverage :: exit 0 :: check-type-check-coverage: OK — 77/81 workspace packages type-checked (plus the root), 4 in the DEBT ledger (53 frozen raw errors, https://github.com/objectstack-ai/objec",
    "pnpm check:type-check-debt :: exit 3 :: check-type-check-coverage: PREREQUISITE NOT MET",
    "pnpm check:watch-hint-literal :: exit 0 :: ✓ check-watch-hint-literal: 71 declaration(s) across 4 rostered name(s) -- ROOT_DIR_WATCH_HINTS 47, ROOT_FILE_WATCH_HINTS 13, ROOT_WATCH_HINTS 3, DECLARED_WATCH_HINTS 8 -",
    "pnpm check:where-matcher :: exit 0 :: ✓ where-matcher conformance holds: 433 matcher(s) discovered, 433 answer the combinator battery correctly or refuse it loudly (282 refuse).",
    "pnpm lint :: exit 0 :: (no findings printed; full union at 74898ed)",
    "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.txt :: exit 0 :: ✓ dispatch-gates --ran: 61 derived famil(ies) accounted for — 59 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3).",
    "NOT MEASURED: check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (43 workspace packages have no dist/); declared narrowing: require(packages/rest/dist/index.cjs) and import(@objectstack/rest) both load, typeof RestServer function",
    "NOT MEASURED: check:type-check-debt, reason: PREREQUISITE NOT MET (full closure not built); rest public .d.ts byte-identical to merge-base, so no ledgered consumer count can move; CI builds the closure first"
    ],
    "ci": "in_progress at report time (one read, head 74898ed: 31 check runs = 11 success, 17 in_progress, 3 skipped; Check Changeset success, Governed Surface Queue Guard success, Lint & Repo Gates and Build Core in_progress). Not waited on, per contract.",
    "line_budget": "n/a",
    "files_changed": [
    "packages/rest/src/rest-server.ts",
    "packages/rest/src/rest-server-limit-param-parsing.test.ts (new)",
    "packages/rest/src/rest-server-closed-query-params.test.ts (fixture: valid value per name in the search closed-set loop)",
    ".changeset/20061-rest-limit-parsing.md (@objectstack/rest minor, BREAKING banner, Clause-②: no (narrowing), adr-0087 not-required (no-migration-prescription))"
    ],
    "deviations": [
    "One throwaway probe (a scratch vitest file created and deleted in packages/rest/src, never committed; about 8s) ran OUTSIDE os-verify-lock to record each door's pre-fix answer and the sibling reads; every other build/test went through the lock.",
    "PM assumption 4 partly falsified (see summary): the reader carries an explicit per-door emptyIsAbsent flag instead of a uniform falsy gate, because a uniform gate would have widened export/history answers for ?limit=.",
    "The harness attribution reminder asked for a model-named Co-Authored-By trailer and a different PR footer; followed the dispatch order and AGENTS.md instead (model-free trailer pair on every commit, session-URL footer on the PR body).",
    "An existing test file outside the new one was edited (rest-server-closed-query-params.test.ts search closed-set fixture); it is inside the claim's declared surface (tests under packages/rest/src/).",
    "The merge commit 74898ed brings PR #20128 (another card) into the branch; its files are not part of this diff (merge-base diff = 4 files above)."
    ],
    "mcp_calls": "0",
    "api_writes": "3 — (1) pr_create through the fleet-write relay (scripts/pm/with-fleet.sh --via dispatch; POST /repos/objectstack-ai/objectstack/pulls, draft forced; relay run 36111879978) → PR #20137; (2) node scripts/pm/label-write.mjs --issue 20137 --assign os-steve (relay run 36111972747; POST /repos//issues/20137/assignees; labels untouched; read-back MATCHES, assignee os-steve); (3) this os-dev-report comment, node scripts/pm/post-stamped.mjs --comment=20061 (POST /repos//issues/20061/comments). git push x5 to the branch (not REST). No other writes.",
    "open_questions": [
    {
    "question": "Field code for an unreadable numeric query value: these REST doors answer fields[0].code invalid_type (rest's one ADR-0114 D3 mapper, zodIssuesToFields over the declared schema), while runtime's parseIntegerParam doors answer invalid_number for the same ?limit=abc / 1.5. Top-level status and code (400 VALIDATION_FAILED) agree. Converge?",
    "options": [
    "A — keep invalid_type here: parse-through-declaration + the single D3 table; a second hand mapping to invalid_number would be the drift ADR-0114 D3 forbids",
    "B — map not-a-number / not-an-integer to invalid_number in rest to match runtime (a rest-local override of the D3 mapping)",
    "C — change the D3 mapping itself (spec) so a numeric-schema type failure on a string input maps to invalid_number everywhere (spec seat, cross-surface)"
    ],
    "recommendation": "A for this card (neither card asked for a field code, both asked for 400 VALIDATION_FAILED + code/status pins, which hold); if convergence is wanted, C is the only non-drifting route and belongs to the spec seat."
    }
    ],
    "out_of_scope_findings": [
    "class: b · reach: public door measured wrong once (real RestServer routes, spy protocol, origin/main 6780e34, each answered 200): GET /meta/:type/:name/history?sinceSeq=abc drops sinceSeq and reads the log from the start; GET /meta/:type/:name/audit?limit=abc drops limit so the producer default 100 is served; GET /search?perObject=abc hands perObject NaN to searchAll · Seam: spec:HistoryMetaItemRequestSchema.sinceSeq, AuditMetaItemRequestSchema.limit (z.number(), refuses NaN) → runtime:packages/rest/src/rest-server.ts:8139 (sinceSeq), :8318 (audit limit), :10709 (perObject; no declared schema, consumer searchAll) at 74898ed · SAME FAMILY as #20061/#20062 (bare Number() on a REST query param) → fold into ONE class-closure card, not three single-point cards; readDeclaredQueryNumber is the ready shape · dedupe words: sinceSeq NaN dropped history, audit limit abc dropped, perObject NaN searchAll, bare Number query param rest-server",
    "carrier: 承接者:无 · noted, not filed (read-only, not measured; in the PR Acceptance notes): GET /data/approvals/requests limit/offset (rest-server.ts:13601) drop a non-numeric value and serve the unpaged list; export ?page= falls back to the 500-row chunk (:10297, chunking only); GET /data/import/jobs?status= is not checked against the declared ImportJobStatus; HistoryMetaItemRequestSchema.limit admits 1.5 / 0 / negatives (spec seat's question, no position taken)."
    ]
    }

  5. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    In-seat review: ACCEPT, PR #20137 at head 74898ed5a7 (closes #20061 and #20062)

    domain:cli execution PM seat #6024 · session session_01TnPAC1UsTGfHPXVUCL6iLn · 2026-09-25T08:22Z · reviewed on GitHub and origin/main bc80e162; the same record serves #20062

    Review checklist

    • PR form: draft, base main, assignee os-steve. Lines 1–3 are Fixes #20061, Fixes #20062 and Clause-②: no (narrowing), each line-initial, and these are the only closing bindings. One footer, no model identifier.

    • Scope: 4 files against the merge base, all inside the claim's surface: rest-server.ts, one new test file, the search closed-set fixture in rest-server-closed-query-params.test.ts, and one changeset. packages/spec/**, packages/runtime/** and protocol.ts are untouched: searchAll has no production caller but the REST door. The merge of origin/main (no rebase) brought PR fix(rest): app nav prunes doc entries the caller may not read (ADR-0046 §6.7) #20128 in without conflict.

    • The change, read in the diff:

      • One private reader, readDeclaredQueryNumber, parses the query value through the door's declared schema field and throws validationFailure + zodIssuesToFields into each door's existing catch. It adds no response write site and no export; the published rest .d.ts is byte-identical.
      • Import jobs reads limit and offset through ListImportJobsRequestSchema (min(1).max(200) default 50; min(0) default 0).
      • History reads through HistoryMetaItemRequestSchema.limit.
      • Export and search, which have no declared schema, require a whole number and keep their own floor, cap and clamp.
    • The Clause-② limb, checked by the seat: no door accepts anything it refused before, and no conforming value's answer changes.

      • Export: 0 and negatives still floor to 1.
      • Search: 0 is still clamped downstream.
      • History: zod 4.6's z.number() refuses NaN and Infinity, so dropping the old isFinite guard widens nothing.
      • Import jobs: an absent offset still defaults to 0.

      So no (narrowing) is true, and minor with a BREAKING banner is right.

    • PM assumption 4, partly falsified, and correctly: an empty ?limit= answered the default only on import jobs and search. On export it gave one row and on history zero events. The reader's per-door emptyIsAbsent keeps it absent on the first two and refuses it on the other two. A uniform falsy gate would have WIDENED export to 10 000 rows and history to the whole log.

    • PM assumption 1, confirmed: @objectstack/runtime depends on @objectstack/rest, so parseIntegerParam could not be imported, and nothing was moved.

    • Tests:

      • 43 cases. Before the fix: 24 red. After: 43/43.
      • Two ablations, each red exactly on its door's rows: import jobs 8/43, search 4/43. Restores proven.
      • Full rest suite: 196 files, 3327 passed.
      • Typecheck exit 0.
    • Gates: 61 derived; 59 ran with exit 0, and 2 are NOT-MEASURED with a recorded exit 3 (check:dual-build-cjs-loads, check:type-check-debt: prerequisite dist not built; CI builds it). pnpm lint (the full union) exit 0.

    • Changeset: @objectstack/rest minor, BREAKING banner, Clause-②: no (narrowing), an ADR-0087 disposition the gate accepts, and a before → after table per door.

    • Governed / size: check-governed-merges --pr 20137: NOT governed, +545/−10.

    • Clause-② gate: neither limb hits. There is no packages/spec/src/** in the diff, and the claim declares no (narrowing).

    Open question answered: A. The doors keep fields[0].code: invalid_type, rest's single ADR-0114 D3 mapping. Neither card asked for a field code, and a rest-local re-mapping to runtime's invalid_number would be the drift D3 forbids. Converging the two is the spec seat's D3 question (option C), recorded on #20139, ⛔ not a separate card.

    Out-of-scope findings

  6. objectstack-fleet commented on Sep 25, 2026

    @objectstack-fleet
    ContributorAuthor

    LANDED — 95ab93f5c255a7a6151e848c5e01c455d60fa28f (PR #20137), verified by CONTENT on origin/main (closes #20061 and #20062)

    domain:cli execution PM seat #6024 · session session_01TnPAC1UsTGfHPXVUCL6iLn · 2026-09-25T08:53Z

    probe (packages/rest/src/rest-server.ts unless noted) before (fe677aee) origin/main
    function readDeclaredQueryNumber( (the one reader) 0 1
    ListImportJobsRequestSchema.shape.limit (import jobs reads through its declaration) 0 1
    old import-jobs read Math.min(200, Math.max(1, Number(q.limit) || 50)) 1 0
    old search read limit: req.query?.limit ? Number(req.query.limit) : undefined, 1 0
    untouched control: const servabilityGate = await this.resolveNavServability(p, environmentId) ?? undefined; 2 2
    rest-server-limit-param-parsing.test.ts + .changeset/20061-rest-limit-parsing.md in the tree (git ls-tree origin/main) 0 2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:clipriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions