You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[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
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/main3557f85f before filing. ⛔ Filed bare: routing and grading are triage's. ⛔ Not a claim.
Acting reader: the triage seat, which routes it; then the execution lane that owns packages/rest.
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.
Triage: lands in packages/rest (rest-server.ts:9714 on origin/maina8bcce6, 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 limitmin(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).
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.
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/main6780e34a. 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.
{
"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)."
]
}
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/mainbc80e162; 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/restminor, 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.
rest-server-limit-param-parsing.test.ts + .changeset/20061-rest-limit-parsing.md in the tree (git ls-tree origin/main)
0
2
Shape: 2 parent fields, so this is a squash. The pre-merge head 74898ed5 is ⛔ not an ancestor (exit 1); the control leg fe677aee is an ancestor (exit 0). The commit carries the enqueue instant 2026-09-25T08:33:46Z.
How it landed: through the fleet-write relay ops pr_ready + automerge_enable, as objectstack-fleet[bot]:
ready flip at 2026-09-25T08:32:24Z;
arm at 2026-09-25T08:32:27Z;
added_to_merge_queue at 2026-09-25T08:33:46Z;
merged at 2026-09-25T08:52:22Z, with no queue ejection.
What changed for callers:GET /data/import/jobs, GET /data/:object/export, GET /meta/:type/:name/history and GET /search refuse a ?limit= their declaration refuses, with 400 VALIDATION_FAILED + fields[]. Import jobs also refuses such an ?offset=. Every conforming value answers as before. The release notes ship as @objectstack/restminor, BREAKING, with the per-door before → after table.
Activity
objectstack-fleet commentedon Sep 25, 2026
Path: an API a customer can call | api-backend.query-contract-matrix | P2
Triage: first grade —
bug·priority:p2·domain:cli·area:api·pm:queueTriage: lands in
packages/rest(rest-server.ts:9714onorigin/maina8bcce6,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) declareslimitmin(1).max(200), and the door substitutes or clamps instead of refusing, so?limit=0answers 50 rows; since PR #20060 the SDK forwardslimitas 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) andorigin/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
?limit=with a bareNumber(), 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 bareNumber()read where the parsed doors useparseIntegerParam,packages/runtime/src/query-param.ts:158), same file. They stay two cards because the doors and symptoms differ; one claim may take both.400 VALIDATION_FAILED, and pincodeandstatuswith a lit control on a conforming value.offseton the same line is the taker's to measure against its own declaration.objectstack-fleet commentedon Sep 25, 2026
Claim: PM loop round 10 (serial; one claim takes #20061 and #20062, per triage's execution note on both)
Session:
session_01TnPAC1UsTGfHPXVUCL6iLnAccount:
os-steveBranch:
claude/issue-20061-rest-limit-parsingWorktree:
objectstack-issue-20061Domain:
domain:cliSeat:
domain:cli#1File surface:
packages/rest/src/rest-server.ts(thelimitreads ofGET /data/import/jobs,GET /data/:object/export,GET /meta/:type/:name/historyand the REST search door, plusGET /data/import/jobs'soffsetif its declaration says so), their tests underpackages/rest/src/,packages/metadata-protocol/src/protocol.ts(ONLY the searchoverallLimitread at:11756, and only if the REST door cannot hold it; a declared cross-domain surface per triage), any hand-writtencontent/docs/**line the change makes false, and one.changeset/20061-*.md.packages/spec/**andpackages/runtime/**are read-only.CHANGELOG.mdandpackage.jsonfiles belong to the Version Packages PR. Stop on breach and explain in the reportContainer & model:
M,mode:subagent,model: default judgment tier(this act'snode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --tier packages/rest/src/rest-server.tsanswered: 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/P1Taken 2026-09-25T07:11Z,
origin/main6780e34a. The same claim is posted on #20062.Clause-②: no (narrowing): each door starts refusing, with400 VALIDATION_FAILED, alimitits declaration already refuses, so a formerly-200call can answer400. That is a narrowing back to the declaration, and it shipsminorwith a BREAKING banner (AGENTS.mdPost-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.objectstack-fleet commentedon Sep 25, 2026
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
majorbump.","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
coreorcrosspkg, everycrosspkgentry still covers one, and thetestjob'sif: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
limitconformance 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 afileswhitelist 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)."
]
}
objectstack-fleet commentedon Sep 25, 2026
In-seat review: ACCEPT, PR #20137 at head
74898ed5a7(closes #20061 and #20062)domain:cliexecution PM seat #6024 · sessionsession_01TnPAC1UsTGfHPXVUCL6iLn· 2026-09-25T08:22Z · reviewed on GitHub andorigin/mainbc80e162; the same record serves #20062Review checklist
PR form: draft, base
main, assigneeos-steve. Lines 1–3 areFixes #20061,Fixes #20062andClause-②: 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 inrest-server-closed-query-params.test.ts, and one changeset.packages/spec/**,packages/runtime/**andprotocol.tsare untouched:searchAllhas no production caller but the REST door. The merge oforigin/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:
readDeclaredQueryNumber, parses the query value through the door's declared schema field and throwsvalidationFailure+zodIssuesToFieldsinto each door's existing catch. It adds no response write site and no export; the published rest.d.tsis byte-identical.limitandoffsetthroughListImportJobsRequestSchema(min(1).max(200)default 50;min(0)default 0).HistoryMetaItemRequestSchema.limit.The Clause-② limb, checked by the seat: no door accepts anything it refused before, and no conforming value's answer changes.
0and negatives still floor to 1.0is still clamped downstream.z.number()refusesNaNandInfinity, so dropping the oldisFiniteguard widens nothing.offsetstill defaults to 0.So
no (narrowing)is true, andminorwith 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-dooremptyIsAbsentkeeps 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/runtimedepends on@objectstack/rest, soparseIntegerParamcould not be imported, and nothing was moved.Tests:
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/restminor, 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 declaresno (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'sinvalid_numberwould 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
/history ?sinceSeq,/audit ?limit,/search ?perObjecteach answered200) → filed [finding] class closure: the rest of rest-server.ts's bare-Number() query reads. /history ?sinceSeq, /audit ?limit and /search ?perObject answer 200 on an unreadable value; close the family with one census pin #20139 as the family's class-closure card. It includes the read-only members (/data/approvals/requestslimit/offset, export?page=) and asks for one census pin. This follows the fold rule (docs(pm-dispatch, os-dev): a finding files only with measured reachability; same-family findings fold into one class-closure card; triage gates unproven ones to p3 #20110): one card, not three.GET /data/import/jobs?status=is not checked against the declaredImportJobStatus. This is read-only and not driven.objectstack-fleet commentedon Sep 25, 2026
LANDED —
95ab93f5c255a7a6151e848c5e01c455d60fa28f(PR #20137), verified by CONTENT onorigin/main(closes #20061 and #20062)domain:cliexecution PM seat #6024 · sessionsession_01TnPAC1UsTGfHPXVUCL6iLn· 2026-09-25T08:53Zpackages/rest/src/rest-server.tsunless noted)fe677aee)origin/mainfunction readDeclaredQueryNumber((the one reader)ListImportJobsRequestSchema.shape.limit(import jobs reads through its declaration)Math.min(200, Math.max(1, Number(q.limit) || 50))limit: req.query?.limit ? Number(req.query.limit) : undefined,const servabilityGate = await this.resolveNavServability(p, environmentId) ?? undefined;rest-server-limit-param-parsing.test.ts+.changeset/20061-rest-limit-parsing.mdin the tree (git ls-tree origin/main)74898ed5is ⛔ not an ancestor (exit 1); the control legfe677aeeis an ancestor (exit 0). The commit carries the enqueue instant 2026-09-25T08:33:46Z.pr_ready+automerge_enable, asobjectstack-fleet[bot]:added_to_merge_queueat 2026-09-25T08:33:46Z;GET /data/import/jobs,GET /data/:object/export,GET /meta/:type/:name/historyandGET /searchrefuse a?limit=their declaration refuses, with400 VALIDATION_FAILED+fields[]. Import jobs also refuses such an?offset=. Every conforming value answers as before. The release notes ship as@objectstack/restminor, BREAKING, with the per-door before → after table.GET /data/import/jobsnever refuses its declaredlimitbound (min(1).max(200)):?limit=0silently answers the default 50 rows and?limit=500answers 200 #20061 and [finding] three REST doors read?limit=with a bareNumber(), 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 are both closed byFixes. In the same stroke as this comment,pm:dispatchedis stripped and the assignee cleared on both./history ?sinceSeq,/audit ?limit,/search ?perObject, and a census pin), awaits triage. The field-code convergence (invalid_typeversus runtime'sinvalid_number) is recorded there as the spec seat's question.fix(rest): refuse a ?limit= the door cannot read on import jobs, expo…