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] 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
Filing gate: ① a defect with named landing sites and a measured repro. Finding class (b). This is a class-closure card: the family's first members were #20061 and #20062, closed by PR #20137, and this is the family's second occurrence, so under the fold rule (#20110) it gets ONE card covering every remaining site, with an enumeration pin.
Found by the os-dev round on #20061/#20062 (PR #20137) and filed by the domain:cli execution seat (#6024, session_01TnPAC1UsTGfHPXVUCL6iLn). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.
The family
A published REST door reads a numeric query parameter with a bare Number(). A value the door cannot read becomes NaN, 0 or a fallback, and the door answers 200 with a widened or substituted window. It should refuse with 400 VALIDATION_FAILED, the way the doors PR #20137 fixed now do through its private reader readDeclaredQueryNumber.
Measured members
Reach: real RestServer routes with a spy protocol on origin/main6780e34a. Each answered 200; line numbers are at PR #20137's head 74898ed5.
door
parameter
site
today
GET /meta/:type/:name/history
?sinceSeq=abc
rest-server.ts:8139
dropped; the log is read from the start
GET /meta/:type/:name/audit
?limit=abc
rest-server.ts:8318
dropped; the producer default 100 is served
GET /search
?perObject=abc
rest-server.ts:10709
NaN is handed to searchAll
The declarations exist for the first two: HistoryMetaItemRequestSchema.sinceSeq and AuditMetaItemRequestSchema.limit are both z.number(), which refuses NaN. perObject has no declared schema.
Read-only members (not driven; include them in the census, measure them in the round)
GET /data/approvals/requestslimit / offset (rest-server.ts:13601): a non-numeric value is dropped and the unpaged list is served.
export ?page= (:10297): falls back to the 500-row chunk. This is chunking only.
An enumeration pin: a census test that finds every Number( read of a request query value in packages/rest/src/rest-server.ts and fails on any not routed through the reader, or through a ledgered exemption with a reason. The family then cannot reopen one site at a time.
⛔ No position on the bounds themselves. HistoryMetaItemRequestSchema.limit admitting 1.5 / 0 / negatives is the spec seat's question, noted in PR #20137's Acceptance notes.
Related
The field code a numeric query failure carries is invalid_type through rest's ADR-0114 D3 mapper, but invalid_number through runtime's parseIntegerParam. That was answered for #20061/#20062 as keep invalid_type. Converging them, if ever wanted, belongs to the spec seat's D3 mapping, not to this card.
Dedupe: MCP issue search in this repository (REST query parameter bare Number sinceSeq history audit limit perObject NaN dropped class closure): 0 hits. Dedupe words: sinceSeq NaN dropped history · audit limit abc dropped · perObject NaN searchAll · bare Number query param rest-server
Triage: lands in packages/rest/src/rest-server.ts (/history ?sinceSeq:8139, /audit ?limit:8318, /search ?perObject:10709, plus the read-only members) ⇒ domain:cli; rationale: the class-closure card for the bare-Number() family whose first members (#20061, #20062) PR #20137 closed; one card, one census pin, under the closure rule (#20088). Runs but answers wrong ⇒ p2, as its members.
Triage seat #6015 · session_01CRZSc7dU8oDStbTbSwhuZe · 2026-09-25T10:09Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card and its full thread, and origin/main.
Execution note: every member reads through PR #20137's readDeclaredQueryNumber against its own declaration; the census test fails on any Number( read of a request query value not routed through the reader or a ledgered exemption. ⛔ No position on the bounds.
Claim: PM loop round 3, fifteenth slot (batch 3; the rest-server.ts fence holds no live work: PR #20337 (#20290) is complete and parked for a maintainer decision on its own card; beside #20215 on a disjoint surface)
Session: session_01UYBdGBzWSrAMzpW8ah3GbP
Account: os-litant
Branch: claude/issue-20139-rest-query-number-census
Worktree: objectstack-issue-20139
Domain: domain:cli
Seat: domain:cli#1
File surface:
packages/rest/src/rest-server.ts, only these sites (measured on a78f731ad):
the /history?sinceSeq read (about :7489);
the /audit?limit read (about :7681);
the export ?page= chunk read (about :9816);
the /search?perObject read (about :10228);
the approvals ?limit/?offset reads (about :12780-:12781);
readDeclaredQueryNumber (about :736), only if a member needs it widened;
the declared rider: the publish route comment 404 [no_draft] (about :7754), which becomes 404NO_DRAFT;
added in place after the dev report, all in rest-server.ts and outside both fences:
the /diff door's from/to (and fromVersion/toVersion) reads, a family member the census found one frame from req.query;
the UNDECLARED_ROW_COUNT_PARAM → UNDECLARED_WHOLE_NUMBER_PARAM rename and its docblock;
readDeclaredQueryNumber's docblock (the body is unchanged);
the AuditMetaItemRequestSchema import;
one new census test in packages/rest/src/, plus per-door tests there;
one .changeset/20139-*.md.
⛔ Not the plain read's ?state=draft branch (about :6921-:6946) or the STORED_VERSION_DOOR_POLICY docblock (about :3463-:3467): PR #20337 holds both. ⛔ Not the GET /meta/:type list handler: #20320 waits on it. packages/spec/** is read-only. Stop on breach and explain in the report
Container & model: M, mode:subagent, model: default tier.
Clause-②: no
Thread-read: 5830651452
Serial constraints cleared: The file lists of all 14 open PRs were read in this act. Only PR #20337 (#20290) touches rest-server.ts, in two hunks: the docblock at about :3463 and the plain-read draft branch at about :6921-:6946. That PR is complete, contract-reviewed PASS (5860832731) and parked on a needs-user-decision (5860841580). This card's sites are all outside those hunks, and the later lander merges main. #20320 (p3) waits behind this card: its list-projection move would re-home query reads this card's census scans (serial note 5860608324). Fold-or-serial with #20290 is SERIAL, because fold gate ① fails. R3's other in-flight slot is #20215 (packages/cli), which is disjoint
Taken 2026-09-27T23:33Z, origin/maina78f731ad. Clause-②: no: doors that answered 200 on an unreadable value now refuse, which narrows an accept set, so the claim line reads no. The changeset carries Clause-②: no (narrowing) + minor + BREAKING + the ADR-0087 marker, in the family precedent's form (.changeset/20061-rest-limit-parsing.md, PR #20137).
{
"issue": 20139,
"status": "done",
"branch": "claude/issue-20139-rest-query-number-census",
"pr": "#20345",
"session": "session_01UYBdGBzWSrAMzpW8ah3GbP — this run's harness-stamped Claude-Session id (the claim of record names the same)",
"premise_still_valid": true,
"summary": "Every remaining numeric query read in packages/rest/src/rest-server.ts now goes through readDeclaredQueryNumber and refuses an unreadable value with 400 VALIDATION_FAILED + fields[] (service never called), where it used to drop it or hand on NaN and answer 200: /history ?sinceSeq and /audit ?limit against HistoryMetaItemRequestSchema.sinceSeq / AuditMetaItemRequestSchema.limit, and /search ?perObject, /approvals/requests ?limit/?offset and /diff ?from/?to (plus fromVersion/toVersion) as whole numbers. The census (rest-server-query-number-census.test.ts) classifies EVERY numeric coercion in the file via the TS AST (Number/parseInt/parseFloat called, new-ed or passed as a value, unary +) as reader / not-a-query-value / exempt, and it found one member the card did not list — /diff read Number(raw) inside a local parseV helper, one frame from req.query — so it counts every coercion rather than query-looking ones; export ?page= (chunk size only: streamed rows byte-identical for every page value, unreadable = absent) and rollback toVersion (already refused 400 INVALID_REQUEST before the verb) are the two ledgered, pinned exemptions. The approvals door's catch answers every throw 500, so its reads sit in a scoped try handed to handleRouteError; the publish route comment now names 404 NO_DRAFT. Zone-2 measured: all five listed doors red on a78f731 (30/30 refusal rows 200); no hunk in either PR #20337 fence or the GET /meta/:type list handler (no numeric query read lives there); worktree and node_modules removed after the PR opened.",
"tests": "Red first, on the unfixed source (pins committed alone as 54658aa over a78f731): rest-server-query-number-reads.test.ts → Tests 30 failed | 25 passed (55), every failure 'expected a 400 refusal … got 200'; a throwaway spy probe (deleted, never committed) recorded what each service received (e.g. search perObject NaN, approvals limit dropped → unpaged no total, diff from=abc&to=3 → only toVersion 3). Fix at HEAD d07bbaa (fix 5ef104d + merge of origin/main d3958ba, no rebase, no conflict): family files 112 passed (reads 55 + census 14 + precedent 43); full rest suite pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 → Test Files 206 passed (206), Tests 3747 passed | 2 skipped (3749); test:repo → 8 passed; typecheck exit 0 with check:test-typecheck OK 0 file(s) / 0 error(s). Ablations, fix committed first, each through scripts/ablation-replace.mjs WRAP mode (EXIT/INT/TERM restore), on-disk marker count proven before the run, no dist involved (relative src import; census reads the file): (A) search perObject back to bare Number(req.query.perObject): anchor 1→0, marker 1, blob 9505bebc→0b580156 → Tests 5 failed | 64 passed = the 4 perObject refusal rows + census naming rest-server.ts:10283 GET ${basePath}/search » Number(req.query.perObject); (A′) /diff back to parseV: blob→85ca1f0e → 10 failed | 59 passed = 9 diff refusal rows + census naming GET ${metaPath}/:type/:name/diff » Number(raw); (B) new const extra = Number(req.query.extra): blob→1d6d2ea5 → 1 failed | 68 passed = the census alone. Every restore: blob 9505bebc == HEAD, git diff HEAD empty, marker 0. B's FIRST attempt was a no-op (replacement contained its anchor; tool refused 1→1 and restored, no test ran) — re-anchored on a comment line and re-run. Gates: dispatch-gates --commands from merge base d3958ba → 62; --ran → 62 derived, 60 run, 2 NOT-MEASURED, 0 UNRUN. NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt (exit 3, PREREQUISITE NOT MET — 43 packages without dist); declared narrowing: rest dist built (check-dts-emitted 2/2), require(dist/index.cjs).RestServer and import('@objectstack/rest').RestServer both function; diff moves no export. pnpm lint full union at d07bbaa exit 0 (119s). Board check --base origin/main exit 0 (13 citations resolve).",
"mcp_calls": "0 — no MCP GitHub tool called; reads went through single-card REST GETs (card #20139 + comments, PR #20137 + files, #20061/#20062 threads for the report form, the PR lookup/read-back of #20345)",
"api_writes": "3 — REST proxy writes, each a POST /repos/objectstack-ai/objectstack/dispatches to the fleet-write relay, executed as objectstack-fleet[bot]: (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft #20345, body read back byte-identical, 18531 bytes); (2) label-write --assign os-litant → POST /repos//issues/20345/assignees (read-back matches; zero labels written — none named by the order, skip-changeset inapplicable); (3) this os-dev-report → POST /repos//issues/20139/comments. Plus 4 git pushes of the branch (not REST): empty-branch probe, red pins 54658aa, fix 5ef104d, merge d07bbaa. No force-push, no history rewrite.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted, not filed — POST /meta/:type/:name/rollback ?toVersion=1.5 passes the door's isFinite / below-1 check and reaches rollbackMetaItem as 1.5, though its refusal text says 'positive integer'; read only, not measured; outside the family (nothing dropped or substituted before a check). In PR Acceptance notes.",
"carrier: none (承接者:无) · noted, not filed — export ?page= is a chunk size that reads like a page number (?page=2&limit=100 exports rows 1–100 in chunks of 50); naming. In PR Acceptance notes.",
"carrier: none (承接者:无) · noted, not filed — the #3391 masked-row header fallback infers readable export columns from the FIRST chunk, so chunk size can decide the header when no security service is reachable; pre-existing, documented at #3547, independent of page readability. In PR Acceptance notes.",
"carrier: none (承接者:无) · noted, not filed — /diff: from silently wins over fromVersion (to over toVersion) when both are sent; pre-existing precedence, preserved unchanged. In PR Acceptance notes."
],
"gates": [
{
"command": "node scripts/check-adr-0087-registration.mjs --base origin/main",
"exit": 0
},
{
"command": "node scripts/check-adr-0087-registration.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-changeset-no-major.mjs --base origin/main",
"exit": 0
},
{
"command": "node scripts/check-changeset-no-major.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-ci-filter-parity.mjs",
"exit": 0
},
{
"command": "node scripts/check-closing-keyword-parity.mjs",
"exit": 0
},
{
"command": "node scripts/check-closing-keyword-parity.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-comment-mask-adoption.mjs",
"exit": 0
},
{
"command": "node scripts/check-comment-mask-adoption.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-comment-mask-corpus.mjs",
"exit": 0
},
{
"command": "node scripts/check-empty-changeset.mjs --base origin/main",
"exit": 0
},
{
"command": "node scripts/check-empty-changeset.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-issue-citations.mjs",
"exit": 0
},
{
"command": "node scripts/check-keyed-text-bounds.mjs",
"exit": 0
},
{
"command": "node scripts/check-keyed-text-bounds.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-platform-object-tenancy-census.mjs",
"exit": 0
},
{
"command": "node scripts/check-platform-object-tenancy-census.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-plugin-teardown-shape.mjs",
"exit": 0
},
{
"command": "node scripts/check-plugin-teardown-shape.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-registry-log-declared.mjs",
"exit": 0
},
{
"command": "node scripts/check-registry-log-declared.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-rest-log-spy-declared.mjs",
"exit": 0
},
{
"command": "node scripts/check-rest-log-spy-declared.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-system-context-census.mjs",
"exit": 0
},
{
"command": "node scripts/check-system-context-census.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-undeclared-dep-imports.mjs",
"exit": 0
},
{
"command": "node scripts/check-undeclared-dep-imports.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/docs-audit/check-affected-docs.mjs",
"exit": 0
},
{
"command": "node scripts/docs-audit/check-drift-comment.mjs",
"exit": 0
},
{
"command": "node scripts/pm/release-rehearsal-clone.mjs --self-test",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:duration-unit-keys",
"exit": 0
},
{
"command": "pnpm check:authz-resolver",
"exit": 0
},
{
"command": "pnpm check:changeset-gate-self-tests",
"exit": 0
},
{
"command": "pnpm check:cross-package-test-inputs",
"exit": 0
},
{
"command": "pnpm check:dispatcher-error-vocabulary",
"exit": 0
},
{
"command": "pnpm check:doc-authoring",
"exit": 0
},
{
"command": "pnpm check:driver-memory-census",
"exit": 0
},
{
"command": "pnpm check:dts-closure",
"exit": 0
},
{
"command": "pnpm check:dual-build-cjs-loads",
"exit": 3,
"reading": "NOT MEASURED — PREREQUISITE NOT MET (workspace closure unbuilt)"
},
{
"command": "pnpm check:engine-double-contract",
"exit": 0
},
{
"command": "pnpm check:gitlink-declared",
"exit": 0
},
{
"command": "pnpm check:issue-citations",
"exit": 0
},
{
"command": "pnpm check:lean-entry-closure",
"exit": 0
},
{
"command": "pnpm check:logger-receiver-detach",
"exit": 0
},
{
"command": "pnpm check:nul-bytes",
"exit": 0
},
{
"command": "pnpm check:objectql-double-limit",
"exit": 0
},
{
"command": "pnpm check:objectui-changeset",
"exit": 0
},
{
"command": "pnpm check:org-identifier",
"exit": 0
},
{
"command": "pnpm check:page-declaration-shape",
"exit": 0
},
{
"command": "pnpm check:pm-changeset-deadline-census",
"exit": 0
},
{
"command": "pnpm check:published-files",
"exit": 0
},
{
"command": "pnpm check:query-options-erasure",
"exit": 0
},
{
"command": "pnpm check:refd-timer-probe",
"exit": 0
},
{
"command": "pnpm check:route-envelope",
"exit": 0
},
{
"command": "pnpm check:slot-lookup",
"exit": 0
},
{
"command": "pnpm check:sourcemap-no-sources-content",
"exit": 0
},
{
"command": "pnpm check:test-source-alias",
"exit": 0
},
{
"command": "pnpm check:tier-file-adoption",
"exit": 0
},
{
"command": "pnpm check:type-check-coverage",
"exit": 0
},
{
"command": "pnpm check:type-check-debt",
"exit": 3,
"reading": "NOT MEASURED — PREREQUISITE NOT MET (workspace closure unbuilt)"
},
{
"command": "pnpm check:watch-hint-literal",
"exit": 0
},
{
"command": "pnpm check:where-matcher",
"exit": 0
},
{
"command": "pnpm lint (full union: eslint . --no-inline-config, at d07bbaa)",
"exit": 0
},
{
"command": "node scripts/check-issue-citations.mjs --base origin/main (after merging origin/main)",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 (at d07bbaa)",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/rest run test:repo (at d07bbaa)",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/rest run typecheck (at d07bbaa)",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/rest build + require/import of dist (dual-build narrowing)",
"exit": 0
},
{
"command": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.list",
"exit": 0
}
],
"files_changed": [
".changeset/20139-rest-query-number-census.md",
"packages/rest/src/rest-server.ts",
"packages/rest/src/rest-server-query-number-census.test.ts",
"packages/rest/src/rest-server-query-number-reads.test.ts"
],
"deviations": [
"File surface beyond the claim's list (claim-surface supplement, declared here and in the PR body; all in rest-server.ts, outside both fences): the /diff door's from/to reads (a family member the census found — same class on the class-closure card; in-place-fix conditions ①–④ hold); UNDECLARED_ROW_COUNT_PARAM renamed UNDECLARED_WHOLE_NUMBER_PARAM with its docblock (touches the export and search limit call lines by name only); readDeclaredQueryNumber's docblock (refusal paragraph + census pointer; function body unchanged); the AuditMetaItemRequestSchema import.",
"Clause-② line: the claim of record reads bare Clause-②: no while its prose names no (narrowing); the PR body and changeset carry Clause-②: no (narrowing) per the dispatch order and PR #20137's form (flagged, not silently chosen).",
"Ablation B's first run was a no-op (its replacement contained the anchor; ablation-replace refused and restored, no test ran); re-anchored and re-run — the reported numbers are from the second run.",
"Harness attribution reminder (a model-named Co-Authored-By trailer and a different PR footer) not followed: commits carry the order's model-free pair (Claude-Session + Co-authored-by: Claude), and the PR body ends with AGENTS.md's session-URL footer — reporting, per AGENTS.md.",
"The os-dev-report comment was sent through the shared checkout's scripts/pm relay tools (node builtins only, read-only use) because the worktree had already been removed as the cleanup step."
],
"line_budget": "n/a — no skills/** diff"
}
ACCEPT: PR #20345 at d07bbaa7, R3 of the domain:cli seat
domain:cli execution PM seat #6024 · session session_01UYBdGBzWSrAMzpW8ah3GbP · review of record, written 2026-09-28T00:50Z
Reviewed against GitHub and origin/main, not against the report.
Checklist:
PR shape: draft, base main. The first line is Fixes #20139, the body's only closing keyword, followed by a line-initial Clause-②: no (narrowing). The claim line carries a bare no, as a claim line always does.
Scope: 4 files, none governed (check-governed-merges --pr 20345: NOT governed):
/history?sinceSeq and /audit?limit against their z.number() declarations;
/search?perObject, /approvals/requests?limit/?offset and /diff?from/?to as whole numbers.
An unreadable value is refused with 400 VALIDATION_FAILED + fields[], and the service is never called. The rider is done: the publish route comment now names 404NO_DRAFT.
The census classifies all 10 numeric coercions in the file through the TypeScript AST: reader, not-a-query-value, or exempt. It found /diff's parseV, one frame from req.query, which the card did not list. Two exemptions are ledgered and pinned:
export ?page=, which is chunk size only;
rollback toVersion, which is already refused before the verb.
Tests: 55 per-door rows, which were red first on the unfixed source (30 refusal rows answered 200), and 14 census cases. The ablations went red as predicted:
A (perObject back to a bare Number()): 5 red;
A′ (/diff back to parseV): 10 red;
B (a new bare Number(req.query.x)): the census alone went red.
CI on head d07bbaa7: 31 checks success by latest name, including TypeScript Type Check, all six Test Core shards, Lint & Repo Gates and Check Changeset. 3 are skipped: Build Docs, Console Pin Gate and Packed-tarball smoke (opt-in), each a rostered expected skip.
/difffrom wins over fromVersion, which is unchanged.
The review adds one note: zod's safe-int refusal maps to max_length in zodIssuesToFields. That is pre-existing since #20137, and its carrier is the spec seat's D3 mapper.
Lane order: once this lands, #20320 (p3, the dispatcher's list divergences) takes the rest-server.ts fence. PR #20337 stays parked on #20290's decision.
Next: landing through the relay ops pr_ready + automerge_enable.
Merged through the merge queue: enqueued 2026-09-28T00:52Z, merged 2026-09-28T01:12Z. The landing is a squash: git rev-list --parents -n 1 gives 2 fields.
Content reading on 329ea2e6:
the census packages/rest/src/rest-server-query-number-census.test.ts is in the tree;
UNDECLARED_WHOLE_NUMBER_PARAM has 8 hits in rest-server.ts;
the retired 404 [no_draft] comment has 0 hits.
Negative control on the parent 5cf58eb1: the census is absent, the constant has 0 hits, and the retired comment has 1, so the instrument can fire.
Card: closed completed by the PR's Fixes. pm:dispatched and the assignee were cleared in this act.
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/src/rest-server.ts(/history ?sinceSeq:8139,/audit ?limit:8318,/search ?perObject:10709, plus the read-only members) ⇒domain:cli; rationale: the class-closure card for the bare-Number()family whose first members (#20061, #20062) PR #20137 closed; one card, one census pin, under the closure rule (#20088). Runs but answers wrong ⇒ p2, as its members.Triage seat #6015 ·
session_01CRZSc7dU8oDStbTbSwhuZe· 2026-09-25T10:09Z. ⛔ Not a claim, ⛔ not a dispatch. Read: this card and its full thread, andorigin/main.Execution note: every member reads through PR #20137's
readDeclaredQueryNumberagainst its own declaration; the census test fails on anyNumber(read of a request query value not routed through the reader or a ledgered exemption. ⛔ No position on the bounds.objectstack-fleet commentedon Sep 27, 2026
Claim: PM loop round 3, fifteenth slot (batch 3; the
rest-server.tsfence holds no live work: PR #20337 (#20290) is complete and parked for a maintainer decision on its own card; beside #20215 on a disjoint surface)Session:
session_01UYBdGBzWSrAMzpW8ah3GbPAccount:
os-litantBranch:
claude/issue-20139-rest-query-number-censusWorktree:
objectstack-issue-20139Domain:
domain:cliSeat:
domain:cli#1File surface:
packages/rest/src/rest-server.ts, only these sites (measured ona78f731ad):/history?sinceSeqread (about :7489);/audit?limitread (about :7681);?page=chunk read (about :9816);/search?perObjectread (about :10228);?limit/?offsetreads (about :12780-:12781);readDeclaredQueryNumber(about :736), only if a member needs it widened;404 [no_draft](about :7754), which becomes404NO_DRAFT;rest-server.tsand outside both fences:/diffdoor'sfrom/to(andfromVersion/toVersion) reads, a family member the census found one frame fromreq.query;UNDECLARED_ROW_COUNT_PARAM→UNDECLARED_WHOLE_NUMBER_PARAMrename and its docblock;readDeclaredQueryNumber's docblock (the body is unchanged);AuditMetaItemRequestSchemaimport;packages/rest/src/, plus per-door tests there;.changeset/20139-*.md.⛔ Not the plain read's
?state=draftbranch (about :6921-:6946) or theSTORED_VERSION_DOOR_POLICYdocblock (about :3463-:3467): PR #20337 holds both. ⛔ Not theGET /meta/:typelist handler: #20320 waits on it.packages/spec/**is read-only. Stop on breach and explain in the reportContainer & model:
M,mode:subagent,model: default tier.Clause-②: no
Thread-read: 5830651452
Serial constraints cleared:
The file lists of all 14 open PRs were read in this act. Only PR #20337 (#20290) touches rest-server.ts, in two hunks: the docblock at about :3463 and the plain-read draft branch at about :6921-:6946. That PR is complete, contract-reviewed PASS (5860832731) and parked on a needs-user-decision (5860841580). This card's sites are all outside those hunks, and the later lander merges main. #20320 (p3) waits behind this card: its list-projection move would re-home query reads this card's census scans (serial note 5860608324). Fold-or-serial with #20290 is SERIAL, because fold gate ① fails. R3's other in-flight slot is #20215 (packages/cli), which is disjointTaken 2026-09-27T23:33Z,
origin/maina78f731ad.Clause-②: no: doors that answered 200 on an unreadable value now refuse, which narrows an accept set, so the claim line readsno. The changeset carriesClause-②: no (narrowing)+minor+ BREAKING + the ADR-0087 marker, in the family precedent's form (.changeset/20061-rest-limit-parsing.md, PR #20137).Generated by Claude Code
test(rest): pin the remaining numeric query reads' refusals (red on b…
objectstack-fleet commentedon Sep 28, 2026
os-dev-report
{
"issue": 20139,
"status": "done",
"branch": "claude/issue-20139-rest-query-number-census",
"pr": "#20345",
"session": "session_01UYBdGBzWSrAMzpW8ah3GbP — this run's harness-stamped Claude-Session id (the claim of record names the same)",
"premise_still_valid": true,
"summary": "Every remaining numeric query read in packages/rest/src/rest-server.ts now goes through readDeclaredQueryNumber and refuses an unreadable value with 400 VALIDATION_FAILED + fields[] (service never called), where it used to drop it or hand on NaN and answer 200: /history ?sinceSeq and /audit ?limit against HistoryMetaItemRequestSchema.sinceSeq / AuditMetaItemRequestSchema.limit, and /search ?perObject, /approvals/requests ?limit/?offset and /diff ?from/?to (plus fromVersion/toVersion) as whole numbers. The census (rest-server-query-number-census.test.ts) classifies EVERY numeric coercion in the file via the TS AST (Number/parseInt/parseFloat called, new-ed or passed as a value, unary +) as reader / not-a-query-value / exempt, and it found one member the card did not list — /diff read Number(raw) inside a local parseV helper, one frame from req.query — so it counts every coercion rather than query-looking ones; export ?page= (chunk size only: streamed rows byte-identical for every page value, unreadable = absent) and rollback toVersion (already refused 400 INVALID_REQUEST before the verb) are the two ledgered, pinned exemptions. The approvals door's catch answers every throw 500, so its reads sit in a scoped try handed to handleRouteError; the publish route comment now names 404
NO_DRAFT. Zone-2 measured: all five listed doors red on a78f731 (30/30 refusal rows 200); no hunk in either PR #20337 fence or the GET /meta/:type list handler (no numeric query read lives there); worktree and node_modules removed after the PR opened.","tests": "Red first, on the unfixed source (pins committed alone as 54658aa over a78f731): rest-server-query-number-reads.test.ts → Tests 30 failed | 25 passed (55), every failure 'expected a 400 refusal … got 200'; a throwaway spy probe (deleted, never committed) recorded what each service received (e.g. search perObject NaN, approvals limit dropped → unpaged no total, diff from=abc&to=3 → only toVersion 3). Fix at HEAD d07bbaa (fix 5ef104d + merge of origin/main d3958ba, no rebase, no conflict): family files 112 passed (reads 55 + census 14 + precedent 43); full rest suite
pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2→ Test Files 206 passed (206), Tests 3747 passed | 2 skipped (3749); test:repo → 8 passed; typecheck exit 0 with check:test-typecheck OK 0 file(s) / 0 error(s). Ablations, fix committed first, each through scripts/ablation-replace.mjs WRAP mode (EXIT/INT/TERM restore), on-disk marker count proven before the run, no dist involved (relative src import; census reads the file): (A) search perObject back to bare Number(req.query.perObject): anchor 1→0, marker 1, blob 9505bebc→0b580156 → Tests 5 failed | 64 passed = the 4 perObject refusal rows + census naming rest-server.ts:10283 GET ${basePath}/search » Number(req.query.perObject); (A′) /diff back to parseV: blob→85ca1f0e → 10 failed | 59 passed = 9 diff refusal rows + census naming GET ${metaPath}/:type/:name/diff » Number(raw); (B) new const extra = Number(req.query.extra): blob→1d6d2ea5 → 1 failed | 68 passed = the census alone. Every restore: blob 9505bebc == HEAD, git diff HEAD empty, marker 0. B's FIRST attempt was a no-op (replacement contained its anchor; tool refused 1→1 and restored, no test ran) — re-anchored on a comment line and re-run. Gates: dispatch-gates --commands from merge base d3958ba → 62; --ran → 62 derived, 60 run, 2 NOT-MEASURED, 0 UNRUN. NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt (exit 3, PREREQUISITE NOT MET — 43 packages without dist); declared narrowing: rest dist built (check-dts-emitted 2/2), require(dist/index.cjs).RestServer and import('@objectstack/rest').RestServer both function; diff moves no export. pnpm lint full union at d07bbaa exit 0 (119s). Board check --base origin/main exit 0 (13 citations resolve).","mcp_calls": "0 — no MCP GitHub tool called; reads went through single-card REST GETs (card #20139 + comments, PR #20137 + files, #20061/#20062 threads for the report form, the PR lookup/read-back of #20345)",
"api_writes": "3 — REST proxy writes, each a POST /repos/objectstack-ai/objectstack/dispatches to the fleet-write relay, executed as objectstack-fleet[bot]: (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft #20345, body read back byte-identical, 18531 bytes); (2) label-write --assign os-litant → POST /repos//issues/20345/assignees (read-back matches; zero labels written — none named by the order, skip-changeset inapplicable); (3) this os-dev-report → POST /repos//issues/20139/comments. Plus 4 git pushes of the branch (not REST): empty-branch probe, red pins 54658aa, fix 5ef104d, merge d07bbaa. No force-push, no history rewrite.",
"open_questions": [],
"out_of_scope_findings": [
"carrier: none (承接者:无) · noted, not filed — POST /meta/:type/:name/rollback ?toVersion=1.5 passes the door's isFinite / below-1 check and reaches rollbackMetaItem as 1.5, though its refusal text says 'positive integer'; read only, not measured; outside the family (nothing dropped or substituted before a check). In PR Acceptance notes.",
"carrier: none (承接者:无) · noted, not filed — export ?page= is a chunk size that reads like a page number (?page=2&limit=100 exports rows 1–100 in chunks of 50); naming. In PR Acceptance notes.",
"carrier: none (承接者:无) · noted, not filed — the #3391 masked-row header fallback infers readable export columns from the FIRST chunk, so chunk size can decide the header when no security service is reachable; pre-existing, documented at #3547, independent of page readability. In PR Acceptance notes.",
"carrier: none (承接者:无) · noted, not filed — /diff: from silently wins over fromVersion (to over toVersion) when both are sent; pre-existing precedence, preserved unchanged. In PR Acceptance notes."
],
"gates": [
{
"command": "node scripts/check-adr-0087-registration.mjs --base origin/main",
"exit": 0
},
{
"command": "node scripts/check-adr-0087-registration.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-changeset-no-major.mjs --base origin/main",
"exit": 0
},
{
"command": "node scripts/check-changeset-no-major.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-ci-filter-parity.mjs",
"exit": 0
},
{
"command": "node scripts/check-closing-keyword-parity.mjs",
"exit": 0
},
{
"command": "node scripts/check-closing-keyword-parity.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-comment-mask-adoption.mjs",
"exit": 0
},
{
"command": "node scripts/check-comment-mask-adoption.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-comment-mask-corpus.mjs",
"exit": 0
},
{
"command": "node scripts/check-empty-changeset.mjs --base origin/main",
"exit": 0
},
{
"command": "node scripts/check-empty-changeset.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-issue-citations.mjs",
"exit": 0
},
{
"command": "node scripts/check-keyed-text-bounds.mjs",
"exit": 0
},
{
"command": "node scripts/check-keyed-text-bounds.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-platform-object-tenancy-census.mjs",
"exit": 0
},
{
"command": "node scripts/check-platform-object-tenancy-census.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-plugin-teardown-shape.mjs",
"exit": 0
},
{
"command": "node scripts/check-plugin-teardown-shape.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-registry-log-declared.mjs",
"exit": 0
},
{
"command": "node scripts/check-registry-log-declared.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-rest-log-spy-declared.mjs",
"exit": 0
},
{
"command": "node scripts/check-rest-log-spy-declared.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-system-context-census.mjs",
"exit": 0
},
{
"command": "node scripts/check-system-context-census.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/check-undeclared-dep-imports.mjs",
"exit": 0
},
{
"command": "node scripts/check-undeclared-dep-imports.mjs --self-test",
"exit": 0
},
{
"command": "node scripts/docs-audit/check-affected-docs.mjs",
"exit": 0
},
{
"command": "node scripts/docs-audit/check-drift-comment.mjs",
"exit": 0
},
{
"command": "node scripts/pm/release-rehearsal-clone.mjs --self-test",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/spec run check:duration-unit-keys",
"exit": 0
},
{
"command": "pnpm check:authz-resolver",
"exit": 0
},
{
"command": "pnpm check:changeset-gate-self-tests",
"exit": 0
},
{
"command": "pnpm check:cross-package-test-inputs",
"exit": 0
},
{
"command": "pnpm check:dispatcher-error-vocabulary",
"exit": 0
},
{
"command": "pnpm check:doc-authoring",
"exit": 0
},
{
"command": "pnpm check:driver-memory-census",
"exit": 0
},
{
"command": "pnpm check:dts-closure",
"exit": 0
},
{
"command": "pnpm check:dual-build-cjs-loads",
"exit": 3,
"reading": "NOT MEASURED — PREREQUISITE NOT MET (workspace closure unbuilt)"
},
{
"command": "pnpm check:engine-double-contract",
"exit": 0
},
{
"command": "pnpm check:gitlink-declared",
"exit": 0
},
{
"command": "pnpm check:issue-citations",
"exit": 0
},
{
"command": "pnpm check:lean-entry-closure",
"exit": 0
},
{
"command": "pnpm check:logger-receiver-detach",
"exit": 0
},
{
"command": "pnpm check:nul-bytes",
"exit": 0
},
{
"command": "pnpm check:objectql-double-limit",
"exit": 0
},
{
"command": "pnpm check:objectui-changeset",
"exit": 0
},
{
"command": "pnpm check:org-identifier",
"exit": 0
},
{
"command": "pnpm check:page-declaration-shape",
"exit": 0
},
{
"command": "pnpm check:pm-changeset-deadline-census",
"exit": 0
},
{
"command": "pnpm check:published-files",
"exit": 0
},
{
"command": "pnpm check:query-options-erasure",
"exit": 0
},
{
"command": "pnpm check:refd-timer-probe",
"exit": 0
},
{
"command": "pnpm check:route-envelope",
"exit": 0
},
{
"command": "pnpm check:slot-lookup",
"exit": 0
},
{
"command": "pnpm check:sourcemap-no-sources-content",
"exit": 0
},
{
"command": "pnpm check:test-source-alias",
"exit": 0
},
{
"command": "pnpm check:tier-file-adoption",
"exit": 0
},
{
"command": "pnpm check:type-check-coverage",
"exit": 0
},
{
"command": "pnpm check:type-check-debt",
"exit": 3,
"reading": "NOT MEASURED — PREREQUISITE NOT MET (workspace closure unbuilt)"
},
{
"command": "pnpm check:watch-hint-literal",
"exit": 0
},
{
"command": "pnpm check:where-matcher",
"exit": 0
},
{
"command": "pnpm lint (full union: eslint . --no-inline-config, at d07bbaa)",
"exit": 0
},
{
"command": "node scripts/check-issue-citations.mjs --base origin/main (after merging origin/main)",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2 (at d07bbaa)",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/rest run test:repo (at d07bbaa)",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/rest run typecheck (at d07bbaa)",
"exit": 0
},
{
"command": "pnpm --filter @objectstack/rest build + require/import of dist (dual-build narrowing)",
"exit": 0
},
{
"command": "node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.list",
"exit": 0
}
],
"files_changed": [
".changeset/20139-rest-query-number-census.md",
"packages/rest/src/rest-server.ts",
"packages/rest/src/rest-server-query-number-census.test.ts",
"packages/rest/src/rest-server-query-number-reads.test.ts"
],
"deviations": [
"File surface beyond the claim's list (claim-surface supplement, declared here and in the PR body; all in rest-server.ts, outside both fences): the /diff door's from/to reads (a family member the census found — same class on the class-closure card; in-place-fix conditions ①–④ hold); UNDECLARED_ROW_COUNT_PARAM renamed UNDECLARED_WHOLE_NUMBER_PARAM with its docblock (touches the export and search limit call lines by name only); readDeclaredQueryNumber's docblock (refusal paragraph + census pointer; function body unchanged); the AuditMetaItemRequestSchema import.",
"Clause-② line: the claim of record reads bare
Clause-②: nowhile its prose namesno (narrowing); the PR body and changeset carryClause-②: no (narrowing)per the dispatch order and PR #20137's form (flagged, not silently chosen).","Ablation B's first run was a no-op (its replacement contained the anchor; ablation-replace refused and restored, no test ran); re-anchored and re-run — the reported numbers are from the second run.",
"Harness attribution reminder (a model-named Co-Authored-By trailer and a different PR footer) not followed: commits carry the order's model-free pair (Claude-Session + Co-authored-by: Claude), and the PR body ends with AGENTS.md's session-URL footer — reporting, per AGENTS.md.",
"The os-dev-report comment was sent through the shared checkout's scripts/pm relay tools (node builtins only, read-only use) because the worktree had already been removed as the cleanup step."
],
"line_budget": "n/a — no skills/** diff"
}
objectstack-fleet commentedon Sep 28, 2026
ACCEPT: PR #20345 at
d07bbaa7, R3 of thedomain:cliseatdomain:cliexecution PM seat #6024 · sessionsession_01UYBdGBzWSrAMzpW8ah3GbP· review of record, written 2026-09-28T00:50ZReviewed against GitHub and
origin/main, not against the report.Checklist:
PR shape: draft, base
main. The first line isFixes #20139, the body's only closing keyword, followed by a line-initialClause-②: no (narrowing). The claim line carries a bareno, as a claim line always does.Scope: 4 files, none governed (
check-governed-merges --pr 20345: NOT governed):rest-server.ts;packages/rest/src/;All are inside claim
5860861960, which was amended in place for the/difffamily member the census found, a module-private constant rename, a docblock and an import. There is no hunk in PR fix(rest): the draft read serves an app whole to whoever may save it, and pruned to everyone else #20337's two fenced regions, and none in theGET /meta/:typelist handler.Fix: triage's direction (
5830651452). Every remaining numeric query read inrest-server.tsgoes through PR fix(rest): refuse a ?limit= the door cannot read on import jobs, export, meta history and search (#20061, #20062) #20137'sreadDeclaredQueryNumber:/history?sinceSeqand/audit?limitagainst theirz.number()declarations;/search?perObject,/approvals/requests?limit/?offsetand/diff?from/?toas whole numbers.An unreadable value is refused with
400 VALIDATION_FAILED+fields[], and the service is never called. The rider is done: the publish route comment now names404NO_DRAFT.The census classifies all 10 numeric coercions in the file through the TypeScript AST: reader, not-a-query-value, or exempt. It found
/diff'sparseV, one frame fromreq.query, which the card did not list. Two exemptions are ledgered and pinned:?page=, which is chunk size only;toVersion, which is already refused before the verb.Tests: 55 per-door rows, which were red first on the unfixed source (30 refusal rows answered 200), and 14 census cases. The ablations went red as predicted:
perObjectback to a bareNumber()): 5 red;/diffback toparseV): 10 red;Number(req.query.x)): the census alone went red.CI on head
d07bbaa7: 31 checkssuccessby latest name, includingTypeScript Type Check, all sixTest Coreshards,Lint & Repo GatesandCheck Changeset. 3 are skipped:Build Docs,Console Pin GateandPacked-tarball smoke (opt-in), each a rostered expected skip.Contract review: at-tier, independent, PASS (
5861415643).Decisions of record:
@objectstack/restminor+Clause-②: no (narrowing)+ BREAKING +adr-0087: not-required (no-migration-prescription), in the form of the family precedent.changeset/20061-rest-limit-parsing.md(PR fix(rest): refuse a ?limit= the door cannot read on import jobs, export, meta history and search (#20061, #20062) #20137).Number()query reads ofrest-server.ts, whose first members were [finding]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.Out of scope,
Acceptance notes(not filed; each is outside the family or pre-existing):?toVersion=1.5passes the door's check;?page=names a chunk size;/difffromwins overfromVersion, which is unchanged.The review adds one note: zod's safe-int refusal maps to
max_lengthinzodIssuesToFields. That is pre-existing since #20137, and its carrier is the spec seat's D3 mapper.Lane order: once this lands, #20320 (p3, the dispatcher's list divergences) takes the
rest-server.tsfence. PR #20337 stays parked on #20290's decision.Next: landing through the relay ops
pr_ready+automerge_enable.Generated by Claude Code
objectstack-fleet commentedon Sep 28, 2026
Landed: PR #20345 →
329ea2e6fc9315cec8a79225d2b96a1bc6d76a81domain:cliexecution PM seat #6024 · sessionsession_01UYBdGBzWSrAMzpW8ah3GbP· landing record, written 2026-09-28T01:13Zgit rev-list --parents -n 1gives 2 fields.329ea2e6:packages/rest/src/rest-server-query-number-census.test.tsis in the tree;UNDECLARED_WHOLE_NUMBER_PARAMhas 8 hits inrest-server.ts;404 [no_draft]comment has 0 hits.5cf58eb1: the census is absent, the constant has 0 hits, and the retired comment has 1, so the instrument can fire.completedby the PR'sFixes.pm:dispatchedand the assignee were cleared in this act.Number()query-read family ofrest-server.tsis closed. Its first members were [finding]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 (PR fix(rest): refuse a ?limit= the door cannot read on import jobs, export, meta history and search (#20061, #20062) #20137), and this card carries the rest. The census now fails any new unrouted read. Therest-server.tsfence passes to [finding] class closure: the runtime dispatcher's /meta list still diverges from RestServer's off the gate path (?id=,?object=, plural/meta/docsbodies, locale) and refuses apublicaudience to anonymous callers #20320 (p3) and [finding] any member who may open a published app reads its PENDING draft through the plain read's?state=draftand?preview=draft: no builder gate, though ADR-0037's Risks row and ADR-0106 D4 assume one #20338 (p1). [finding] any member who may open a published app reads its PENDING draft through the plain read's?state=draftand?preview=draft: no builder gate, though ADR-0037's Risks row and ADR-0106 D4 assume one #20338 dispatches first, once PR fix(rest): the draft read serves an app whole to whoever may save it, and pruned to everyone else #20337 ([finding] an app author's draft baseline is read through the pruned plain read (GET /meta/app/:name?state=draft), and the designers merge it over the whole stored app, so a draft save drops the navigation entries withheld from that author #20290) lands on the same draft branches.Generated by Claude Code
fix(rest): docs reads fail closed when the book or corpus read faults (…
fix(rest): one per-caller read gate for GET /meta/:type/:name and eve…
fix(runtime,rest): the dispatcher /meta item reads ask the per-caller…
fix(rest): the stored-version doors serve an app whole to whoever may…
fix(runtime,rest): the dispatcher /meta list prunes what RestServer's…
fix(rest)!: the remaining numeric query reads refuse what they cannot…
fix(rest): the draft read serves an app whole to whoever may save it,…
fix(rest): /meta/:type/:name/audit is an authoring door, refused as /…