docs(rest): the batch cap is embedder policy, not deployment policy - #16942
Conversation
`enforceBatchSize`'s docblock called `RestServerConfig.batch.maxBatchSize`
"deployment policy". That is false of every shipped boot path: a
`RestServerConfig` is the argument a host passes at construction, there is
exactly one door (`createRestApiPlugin({ api })`, whose `start()` is the only
non-test site reaching `new RestServer(...)`), and neither shipped boot path
opens it with a `batch` config — `os serve` forwards two `api.*` keys and the
dev plugin passes none. A CLI-started deployment always gets the 200 default
and cannot move it.
Narrow the prose onto that fact using the vocabulary already landed on the
spec side: `Reachability: EMBEDDER-ONLY`, matching the
`BatchEndpointsConfigSchema` docblock, the WHO CAN WRITE THIS CONFIG header in
`packages/spec/src/api/rest-server.zod.ts` and the per-key REACHABILITY row in
`packages/spec/liveness/batch_endpoints.json`. Two wordings for one fact in two
packages is how this defect survived the spec-side fix.
No behaviour change: the 1..1000 range and the 200 default are correct and
still enforced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
…rection Measured, not assumed: the docblock reaches published output. Built before and after the fix and read `packages/rest`'s own `files[]` (dist, README.md, CHANGELOG.md) — `deployment policy` went 1 -> 0 and `EMBEDDER-ONLY` 0 -> 1 in each of dist/index.d.ts, index.d.cts, index.js and index.cjs, and dist/index.d.ts moved 129.91 KB -> 131.47 KB. Positive control on the same paths: `enforceBatchSize` 14 hits, `BATCH_TOO_LARGE` 2. Published bytes move, so `skip-changeset` is not available. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 4 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2693ed3e6a4de7a50e4a30d0dcc428cb488f8cad && git checkout 2693ed3e6a4de7a50e4a30d0dcc428cb488f8cad
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 31e7542b0b4880e86cf26b385c77bf448288db8b 35c8e983dce5da3149d888a9975e7fe18977a382 && git checkout -B drift-repro 31e7542b0b4880e86cf26b385c77bf448288db8b && git merge --no-ff 35c8e983dce5da3149d888a9975e7fe18977a382
node scripts/docs-audit/affected-docs.mjs --json 31e7542b0b4880e86cf26b385c77bf448288db8b
|
PM review — route 1 held, and two of the measurements went the opposite way from the cheap guess.
|
Accepted — CI is fully green on
|
| raw check-run rows | 45 |
| names carrying superseded earlier rows | 6, each appearing 3× (Auto Label, Check PR Size, Check Changeset, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, No other open PR may claim the same issue) — 12 superseded rows |
| distinct names after collapse | 33 |
| failure / cancelled / timed_out | 0 |
| queued / in_progress | 0 |
Three workflow batches ran on this head, so six names carry three rows each — the same shape as #16926 earlier today (also 45 → 33). ⛔ Reading the raw list would have triple-counted them, and ⛔ it changes no verdict either way: none of the 12 superseded rows is a red.
Landing precheck
- Clause ②:
node scripts/pm/check-clause2-carriers.mjs --pair 16942— exit 0, both carriers agree, no widening tell (⚠️ the checker's own caveat kept: a tell is not a proof and its absence is not one either). Governed Surface Queue Guard,Check Changeset, and both single-writer/issue-claim guards green.- Verified at source and unchanged from the review: two files; the docblock's first line corrected as well as the quoted sentence; route 2 fenced in the prose itself;
content/docs/releases/untouched (0 paths in the diff), with all four release-owned rows read and none edited.
The two readings that made this PR worth its round
⭐ The changeset was measured, and the cheap guess was wrong. A source docblock is emitted into dist/index.d.ts / .d.cts / index.js / index.cjs — deployment policy 1→0, EMBEDDER-ONLY 0→1, index.d.ts 129.91 → 131.47 KB, positive control enforceBatchSize 14 — so skip-changeset was not available and the @objectstack/rest patch changeset is required. That reading is now a standing rule for this lane.
⭐ A third carrier was found that no derivation can see — content/docs/api/data-api.mdx, hand-written and operator-facing, calling the cap "the deployment's batch.maxBatchSize (default 200, configurable 1-1000)". Filed as #16940, ⛔ correctly outside this card's fenced surface.
Arming
Ready-for-review first, auto-merge second. merge_method: merge regardless; inert here — the queue squashes. ⛔ I neither approve nor merge.
On merge I will verify the landing on origin/main and strip pm:dispatched from #16801 after reading its current labels.
Generated by Claude Code
Fixes #16801
Clause-②: no
Re-declared from the delivered diff, not inherited from the dispatch. Every hunk is a docblock
comment plus one changeset file. No accept set is relaxed, no published surface gains a member,
no validator,
.default(),.min/.maxor refinement moves. The 1..1000 range and the 200default are byte-identical before and after — the prose is the whole change.
enforceBatchSize's docblock calledRestServerConfig.batch.maxBatchSize"deployment policy". That is false of every shipped boot path, and it is the same claim #15543 already retracted one package over inpackages/spec. This narrows the prose onto the fact that already landed, in the vocabulary that landed with it.⛔ Route 1 only. No
batchconfig is threaded through any boot path — that would open a new authorable key, which the 2026-09-07 ruling (director seat, summon #17, decision batch #2, maintainer verbatim 「同意」) declined for the spec-side siblings for want of measured demand. Reversing it is its own decision card, not an implementation detail.The docblock, before and after
:2071→ dispatch:2079); a line citation is not usable here.Before —
packages/rest/src/rest-server.ts#enforceBatchSize:After (the two untouched middle paragraphs on #3939/#3897 history elided):
Note the first line moved too: "the deployment's batch-size cap" carried the identical false framing and would have survived a fix aimed only at the sentence the card quotes.
Premise re-verified here, with controls
70f7d6d735new RestServer(sitepackages/rest/src/rest-api-plugin.tsinsidestart(). 144*.test.tsfiles hold the rest; the only other non-test hits arerest-server.ts's own@exampleblock and prose inpackages/spec.serve.tsforwards two keyscreateRestApiPlugin({ api: { api: { enableProjectScoping, projectResolution } } as any })— confirmed, two keys,as any.this.childPlugins.push(createRestApiPlugin());— confirmed.maxBatchSizeoccurs 6× inrest-server.tsand across 4 files underpackages/rest/src.The spec-side wording mirrored, and where it was read
⭐ Deliberately not a second phrasing. Read before writing, in the tree at this PR's base:
packages/spec/src/api/rest-server.zod.ts— theBatchEndpointsConfigSchemadocblock: "Reachability: EMBEDDER-ONLY ([finding] No shipped boot path authorsRestServerConfigat all —os servefixes it and the dev plugin passes none, so every livecrud/metadata/batchkey is embedder-only #15543). Every key below is parsed and read at construction, and none of them is authorable from a CLI-started deployment —os servepasses only the twoapi.*keys named in the file header and the dev plugin passes nothing."RestServerConfigat all —os servefixes it and the dev plugin passes none, so every livecrud/metadata/batchkey is embedder-only #15543): "There is exactly ONE door, and it is programmatic", and "This is the recorded posture, not a gap awaiting a fix".packages/spec/liveness/batch_endpoints.json— the per-keyREACHABILITYrow onmaxBatchSize: "embedder-only. Written only by a host that constructs a RestServerConfig; no shipped boot path authors it, so a CLI-started deployment always gets the schema default."The new docblock reuses
Reachability: EMBEDDER-ONLY, "exactly ONE door", "a CLI-started deployment always gets the schema default", and "the recorded posture, not a gap awaiting a fix" verbatim, and points at all three records by path so the next reader finds the other half instead of re-deriving it. Two wordings for one fact in two packages is precisely how this defect survived the spec-side correction.Changeset — measured, and
skip-changesetis not available@objectstack/restpublishesfiles: ["dist", "README.md", "CHANGELOG.md"]. Built the package from the pre-fix source and from the fixed source and read those paths both times:deployment policybefore → afterEMBEDDER-ONLYbefore → afterdist/index.d.tsdist/index.d.ctsdist/index.jsdist/index.cjsdist/index.d.tsalso moved 129.91 KB → 131.47 KB. Positive control on the same paths, proving the search fires:enforceBatchSize14 hits,BATCH_TOO_LARGE2,Batch too large2.⇒ Published bytes move — the docblock ships to consumers inside the
.d.ts, which is where an npm consumer's editor reads it. Apatchchangeset for@objectstack/restis included;skip-changesetwould have been wrong.The before-leg was taken by restoring the pre-fix source with
git checkout HEAD~1 -- ..., rebuilding, measuring, and restoring withgit checkout HEAD -- ...under anEXIT INT TERMtrap using absolute paths. Restore proven by blob hash equality againstHEADplus an emptygit status --porcelain, not by an exit code. The tree was rebuilt from the fixed source afterwards, verified:deployment policy0,EMBEDDER-ONLY1.Docs drift — re-derived, and the tool's list is not a clean bill
Re-derived myself from a clean worktree (
scripts/docs-audit/affected-docs.mjs --json):"dirty": false,diffBase70f7d6d735, head35c8e983dc. It reports 4 docs, every one"releaseOwned": true(releases/implementation-status.mdx,v12,v16,v17), reached through a single anchor: the symbolRestServer. ⛔ Release-owned docs are never edited in a code PR — this PR's only input to them is its changeset.content/deployment policyenforceBatchSizebatch.maxBatchSizemaxBatchSizeRestServerConfigThe four release-owned pages — discriminated, because "read-only" is not "not falsified"
maxBatchSize/batch cap/deployment policyagainst aRestServercontrol:RestServer(control)releases/implementation-status.mdxreleases/v12.mdxreleases/v16.mdxreleases/v17.mdxThe control fires on all four, so the three zeros are readings, not dead greps. Three of the four are broad-anchor artifacts with nothing to judge.
v17.mdx, both sites, read in their sections — my verdict: NOT falsified. ⛔ Edited neither, whatever the verdict.:1936-1937— "The declared 200-row batch cap is now real on all five bulk routes (400 BATCH_TOO_LARGE, governed bybatch.maxBatchSize, 1..1000)". It states that the cap is now enforced, which key governs it, and the key's declared range. It makes no reachability claim at all — no agent, no "deployment", no "configurable". Nothing here is falsified by this card.:4998— "Bulk callers: stay underbatch.maxBatchSize(default 200, raisable to 1000) or chunk". ⭐ The question israisableby whom, and the honest answer is that the sentence is neither of the two candidates: it is not addressed to the embedder, and it does not tell a reader a deployment can raise the cap. Its enclosing section is the v17 migration list, whose every item is addressed to a caller class (SDK callers,List-query callers,Raw-fetch readers,Analytics clients, …), and its instruction to that reader is "stay under it or chunk".raisable to 1000is an aside about the key's declared range, which is true — an embedder constructing aRestServerConfigcan set it up to 1000 — and it names no actor who can move it.⇒ It is weaker than the carrier I did file, and the difference is the whole distinction:⚠️ It remains misleading by omission to an audience that cannot move the value — that is a judgement call, and a successor who weighs the omission more heavily should file it as a dedicated docs-only card rather than reopen this one. ⛔ Not filed here, and ⛔ not folded into the
data-api.mdxcalls the cap "the deployment's" and "configurable 1–1000" — it attributes agency to a reader who has none.v17.mdx:4998attributes agency to nobody. What would flip my verdict is a sentence naming the actor ("your deployment can raise it", "ask your operator"); there is none.enforceBatchSizedocblock as a substitute for fixing a page.It caught one.
content/docs/api/data-api.mdx(hand-written, operator-facing) says the limit is "the deployment'sbatch.maxBatchSize(default 200, configurable 1–1000)" — the third carrier of this same claim. It appears in neither the tool's flagged list nor itspackageMentionDocs. ⛔ Outside this PR's fenced file surface, so it is filed as #16940 and left to that card.docs/qa/platform-checklist/FOLLOW-UPS.md— discharged, and left to its holder§10b E2 records this item as owed. ⛔ Not edited here: the file is HELD by open PR #16909, and hot-file holds are released by merge, not by arming.
Holder reading taken independently at PR time — 22 open PRs, every one contributing at least one file row, so no PR is silently contributing an empty list (PR #15334's 537 files paged to exhaustion at page 6, page 7 empty):
packages/rest/src/rest-server.tscontent/docs/api/data-api.mdxdocs/qa/platform-checklist/FOLLOW-UPS.mdSensitivity control: the same query returns #16909 as the FOLLOW-UPS holder rather than answering "free" to everything. ⇒ The E2 entry is now discharged by this PR. Striking it belongs to whoever lands #16909, or to the next seat that touches that file.
The record correction on #15543
Triage asked that #15543's record be corrected, since it and its ruling attribute the "deployment policy" sentence to
packages/spec/src/api/rest-server.zod.ts, where it does not occur.file#symboland already points at #16801.Re-took the measurement anyway rather than relaying it, because an unre-measured citation is exactly how the original misattribution travelled:
deployment policyoverpackages/spechits 2, both inCHANGELOG.md, and 0 insrc/— including 0 inrest-server.zod.tsitself, against a same-file control ofmaxBatchSize4 and a same-tree control of 11 files. The zeros are readings.⇒ Posted only the half the existing comment could not carry — the landing: #15543 (comment)
Verification
All at final head
35c8e983dc, working tree clean.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(⛔ not a hand-written list), re-derived after the changeset existed because that moves the derivation: 51 families. Reconciled with--ran: "51 derived famil(ies) accounted for — 51 run, 0 NOT-MEASURED". 50 exit 0.pnpm check:dual-build-cjs-loads— NOT MEASURED, not a pass and not a red. Exit 3, its own verdict text: "PREREQUISITE NOT MET — this gate reads built output, and some package has no dist/ … ⛔ This is NOT a pass: nothing was measured." It wants a whole-repo build (43 packages listed); its--self-testleg passed 93 cases. CI checks out fresh and builds, so it is measured there. This diff changes no export, entry orpackage.jsonfield it reads.pnpm --filter @objectstack/rest typecheck, exit 0. Itscheck:test-typecheckleg confirms the test layer compiles underpackages/rest/tsconfig.test.json: "0 file(s) / 0 error(s) / 0 pinned signature(s)", so the typecheck is not silently excluding**/*.test.ts.pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2: 184 test files passed, 3057 tests passed, exit 0.eslint . --no-inline-config, exit 0, 6381 files in eslint's own population (count read from--format json, not guessed), 0 errors, 0 warnings,rest-server.tsconfirmed present in that population. 135s.pnpm --filter '@objectstack/rest...' build(upstream closure + the package), exit 0.pnpm check:nul-bytesgreen, plus a direct sweep of both changed files for the non-NUL control range: no hits.Exit codes were captured before any pipe (
cmd > log 2>&1; EXIT=$?), and each verdict above quotes the gate's own printed line rather than a bare$?.Out of scope, noted not filed
packages/spec/CHANGELOG.mdandpackages/rest/CHANGELOG.mdeach carry the original "deployment policy" sentence twice. Shipped release history compiled from changesets — not retro-edited, and not a defect in this tree. Successor: none; a reader arriving there is reading what v17 shipped.content/docs/releases/v17.mdxstates the cap is "raisable to 1000". RELEASE-OWNED, ⛔ never edited in a code PR; recorded inside [finding]content/docs/api/data-api.mdxtells operators the batch cap is "the deployment's" and "configurable 1-1000" — the third carrier of the claim #15543 and #16801 corrected in code #16940 for whoever takes that card.content/docs/api/data-api.mdxtells operators the batch cap is "the deployment's" and "configurable 1-1000" — the third carrier of the claim #15543 and #16801 corrected in code #16940 shows. Deliberate, and said out loud rather than silently omitted.Generated by Claude Code