Skip to content

fix(service-storage)!: the chunked-upload init door parses its body with the declared request schema - #27781

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-und9e8
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-und9e8

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread
Fixes #27549

Clause-②: no (narrowing — the init door parses its body with InitiateChunkedUploadRequestSchema; a chunkSize below the declared minimum is refused 400, not clamped, per triage 6498840027)
Metadata-protocol: none
Skills-docs: none — no skill or doc teaches the old clamp or a string total; the generated reference (content/docs/references/api/storage.mdx) already states the schema the door now enforces

Card part (a) only. Part (b) (GetPresignedUrlRequestSchema.size) was split to domain:spec by triage 6498840027 and is not touched here.

维护者速读

  • 改了什么:分块上传的开启接口 POST /api/v1/storage/upload/chunked 现在用规范里已声明的 InitiateChunkedUploadRequestSchema 解析请求体,不合规就 400 拒收,点名出错字段。
  • 为什么改:以前只看 totalSize 是否为真值,负数或字符串也能开出一个永远无法完成的上传会话;小于 5 MiB 的 chunkSize 被悄悄抬到 5 MiB,而规范说的是拒收。
  • 风险与回滚:收窄接受面,传字符串 totalSize 或小于 5 MiB chunkSize 的第三方客户端会开始收到 400;SDK 按同一类型发送不受影响。回滚即还原本 PR 的单个 commit。
  • 先看:packages/services/service-storage/src/storage-routes.ts(init 处理器的解析与拒收)、storage-routes.test.ts(新钉子)、.changeset/27549-chunked-init-declared-shape.md。
  • 两分钟验证:pnpm --filter @objectstack/service-storage exec vitest run src/storage-routes.test.ts,预期 70/70 通过,其中 7 条 REFUSED、3 条 CONTROL。
  • 需要你做的:无,席位落地(契约复审 PASS 后)。

What changes

POST /api/v1/storage/upload/chunked (packages/services/service-storage/src/storage-routes.ts) now parses its body with InitiateChunkedUploadRequestSchema before the pending sys_file row, the backend multipart and the sys_upload_session row are written. The handler then reads the parsed values.

  • Refused 400 INVALID_REQUEST, naming each failing field with the schema's own words, with neither row written: a totalSize that is not a whole JSON number of at least 1 (-5, "100", 0, 1.5); a chunkSize that is not a whole JSON number of at least 5242880 (it is no longer clamped up); a bucket that is not a string; a metadata that is not an object of string values.
  • Unchanged: an omitted chunkSize takes the schema's declared default of 5242880, so a valid body mints the same session with the same totalChunks. A missing filename, mimeType or totalSize keeps its existing message, and an off-vocabulary scope keeps its own remedy: the scope gate still runs before the parse. The presence check for totalSize moves from truthiness to == null (the presigned door's shape), so 0 now reaches the parse and is refused by name.
  • Same envelope as the presigned door's refusal from PR fix(service-storage): refuse an out-of-range chunkIndex and a non-byte-count presigned size at the upload doors #27537: sendError(res, 400, 'INVALID_REQUEST', message). No new error code.
  • ADR-0087 disposition: not-required (no-migration-prescription). A REST request body is refused against a spec schema that already declares it, unchanged; no metadata key, export, type or error code moves, so there is nothing for objectstack migrate meta to rewrite. The changeset carries the BREAKING banner and the FROM / TO / Fix account.

Why

InitiateChunkedUploadRequestSchema declares totalSize: int().min(1) and chunkSize: int().min(5242880), and the door enforced neither (declared is not enforced). A negative total minted a session with totalChunks of -0, so every chunk was refused after #27412 and the upload could never complete; a numeric-string total was stored as sent. Triage ruled (6498840027) that declared is enforced and a chunkSize below the minimum is refused, not clamped.

Tests

packages/services/service-storage/src/storage-routes.test.ts, in Chunked upload flow:

  • 7 REFUSED 400 INVALID_REQUEST rows: negative, numeric-string, zero and fractional totalSize; chunkSize 1024; numeric-string chunkSize; a non-string metadata value. Each asserts status, success: false, error.code, that the message names the field, and that createFile and createSession were never called.
  • 3 CONTROL rows: an omitted chunkSize (default 5242880, 3 chunks for 12 000 000 bytes), chunkSize at the minimum (3 chunks) and above it (10485760, 2 chunks). Each asserts the response's chunkSize / totalChunks, the stored session's chunk_size / total_chunks, and the stored file size.
Ablation: drop the parse, then restore

Run with node scripts/ablation-replace.mjs on storage-routes.ts, after the fix was committed (e3c83f13). Anchor const parsed = InitiateChunkedUploadRequestSchema.safeParse(body); replaced with a pass-through { success: true, data: body }. The anchor went from 1 to 0 hits on disk and the blob from 6b8c79453e4b to 86146bd7a141.

  • Mutated: Tests 12 failed | 58 passed (70). All 7 REFUSED rows and the omitted-chunkSize CONTROL went red, plus 4 existing terminal-status tests whose init omits chunkSize (a NaN chunk count without the schema default). The two CONTROL rows that send a chunkSize stayed green, as expected.
  • Restored: blob after restore 6b8c79453e4b equals the HEAD blob, and git diff HEAD is empty. Then Tests 70 passed (70).

The subject is imported from src/ (./storage-routes.js), so no dist rebuild sits between the mutation and the run.

Acceptance notes

  • The parse covers the whole declared body, as the ruling says, so bucket and metadata narrow too (listed in the changeset). null for chunkSize, bucket or metadata was read as omitted and is now refused, because the schema declares them optional, not nullable.
  • filename: '' and mimeType: '' stay refused by the existing presence check. That check is stricter than the schema's z.string(), and this PR leaves it as it was.
  • scope ?? 'user' in the handler body is now redundant (the parse applies the declared default) and is left as it was, to keep the diff small.

Verification

All local runs are on head 26e6d217.

Package tests and typecheck
  • pnpm --filter @objectstack/service-storage test: Test Files 70 passed (70), Tests 1297 passed (1297), lock VERDICT command-exit 0.
  • pnpm --filter @objectstack/service-storage typecheck: exit 0; check:test-typecheck: OK, 0 errors.
  • pnpm lint: exit 0.
Derived gate families (dispatch-gates --commands, 64 families)

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 64 families from the 3 changed paths at 26e6d217. All 64 exited 0. check:i18n and check:dual-build-cjs-loads first exited 3 (prerequisite not met) and passed after the build closure they name: check:i18n OK, 10 packages in sync; check:dual-build-cjs-loads 112 entry points across 69 packages load. --ran reconciliation, with an exit code on every line: 64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED (a DERIVED zero — all 64 recorded an exit code and none of them is 3).

Dogfood battery

Full battery once on the final head 26e6d217 (title carries !, Clause-②: carries the narrowing arm): build closure @objectstack/dogfood^..., then pnpm --filter @objectstack/dogfood test under the verify lock, no shard and no path. Test Files 355 passed | 3 skipped (358), Tests 2867 passed | 20 skipped (2887), lock VERDICT command-exit 0, held 21m00s. No failures.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (packages/spec not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

…ith the declared request schema

POST /storage/upload/chunked checked totalSize for truthiness only and
clamped chunkSize up to the 5 MiB minimum. A negative or string total
minted a session no chunk could ever complete. The body is now parsed
with InitiateChunkedUploadRequestSchema before anything is stored:
a total that is not a positive whole number, a chunkSize below the
declared minimum, or any other off-schema field is refused
400 INVALID_REQUEST naming the field. An omitted chunkSize keeps the
declared 5242880 default.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4d1JR8d3KVqut8R6TWN
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-storage, touching 3 documentable anchor(s).

7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via INVALID_REQUEST (literal, a string literal in registerStorageFamily))
  • content/docs/api/metadata-api.mdx (via INVALID_REQUEST (literal, a string literal in registerStorageFamily))
  • content/docs/automation/webhooks.mdx (via INVALID_REQUEST (literal, a string literal in registerStorageFamily))
  • content/docs/data-modeling/import-mappings.mdx (via INVALID_REQUEST (literal, a string literal in registerStorageFamily))
  • content/docs/permissions/authentication.mdx (via INVALID_REQUEST (literal, a string literal in registerStorageFamily))
  • content/docs/permissions/sso.mdx (via INVALID_REQUEST (literal, a string literal in registerStorageFamily))
  • content/docs/ui/forms.mdx (via INVALID_REQUEST (literal, a string literal in registerStorageFamily))

⛔ 5 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via INVALID_REQUEST (literal, a string literal in registerStorageFamily))
  • content/docs/releases/v17/17-6.mdx (via INVALID_REQUEST (literal, a string literal in registerStorageFamily))
  • content/docs/releases/v18/18-0.mdx (via INVALID_REQUEST (literal, a string literal in registerStorageFamily))
  • content/docs/releases/v18/18-1.mdx (via INVALID_REQUEST (literal, a string literal in registerStorageFamily))
  • content/docs/releases/v18/18-2.mdx (via INVALID_REQUEST (literal, a string literal in registerStorageFamily))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 10 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 968cacfd10fdec64b6089d0da4f721f1a8a10925 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9481bcf3b15ba2e3b00d7c7f6c43276c44a5f187 — the merge of head e77ad2750539a20766866f78c526b8f78b9d2516 into base 968cacfd10fdec64b6089d0da4f721f1a8a10925, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9481bcf3b15ba2e3b00d7c7f6c43276c44a5f187 && git checkout 9481bcf3b15ba2e3b00d7c7f6c43276c44a5f187
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 968cacfd10fdec64b6089d0da4f721f1a8a10925 e77ad2750539a20766866f78c526b8f78b9d2516 && git checkout -B drift-repro 968cacfd10fdec64b6089d0da4f721f1a8a10925 && git merge --no-ff e77ad2750539a20766866f78c526b8f78b9d2516

node scripts/docs-audit/affected-docs.mjs --json 968cacfd10fdec64b6089d0da4f721f1a8a10925

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 968cacfd10fdec64b6089d0da4f721f1a8a10925 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract-review-claim: session_016WV4d1Np3hU7RQnA13YZcL · head 26e6d21 · 2026-10-29T23:02Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 26e6d21749b3f256f1e2aa159f5336f45c1ac8e1
Local-runs: none
Scope: packages/services/service-storage/src/storage-routes.ts, packages/services/service-storage/src/storage-routes.test.ts + packages/spec/src/api/storage.zod.ts (InitiateChunkedUploadRequestSchema, UploadScopeSchema, GetPresignedUrlRequestSchema; byte-identical to main), the presigned door's isDeclaredByteCount parse from PR #27537 and requireAcceptedUploadScope in the same file, packages/client/src/index.ts (storage.initChunkedUpload), every in-repo caller of POST …/upload/chunked (service-storage tests, organizations wall tests, dogfood suites, client wire-dialect test), docs/qa/platform-checklist/areas/attachments-storage.json, ui/ (searched for a Console caller: none); .changeset/27549-chunked-init-declared-shape.md in ② only.

Read-only shape held: the diff (81d00a4e..26e6d217, three files, identical against origin/main), card #27549 with every comment, the head's check-runs. Nothing built, run or re-run; two exported pure readers were called on text alone (readClause2Line on the two Clause-②: lines, findMigrationPrescription / readDisposition on the changeset body) — reads of recognisers, not gate runs.

Check-runs on the head as read at 2026-10-29T23:13Z: success — Build Core, Check Changeset (this job runs check-adr-0087-registration.mjs --base MERGE_BASE), Check PR Size, Auto Label, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint & Repo Gates, Lint · CI/release/engine, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped ×3, Live E2E UI, PR-body guards, Plan Test Core shards, Test UI, Type Check · consumer / debt ledger / source / workspace, TypeScript Type Check, filter, Test Core 1/8, 2/8, 3/8, 4/8, 6/8, 7/8, 8/8. In progress — Test Core 5/8. Skipped (path-filtered) — UI build/lint/typecheck/test shards, Build Docs, Temporal Conformance, opt-in harnesses. No failure read. The Tier S landing still waits for that last shard; this record does not stand in for it.

① Derived judgments

The diff changes ONE door, POST /api/v1/storage/upload/chunked: after the existing presence check and the existing scope gate, the body is parsed with InitiateChunkedUploadRequestSchema.safeParse(body); a failure answers sendError(res, 400, 'INVALID_REQUEST', …) naming each issue as path: message; the handler then reads parsed.data. Every accept-set change this implies, judged against the schema on main (totalSize: z.number().int().min(1), chunkSize: z.number().int().min(5242880).default(5242880), scope: UploadScopeSchema.default('user'), bucket: z.string().optional(), metadata: z.record(z.string(), z.string()).optional()) and the ruling 6498840027 「Declared is enforced」:

  1. totalSize must be a whole JSON number ≥ 1 — right. -5, "100", 0, 1.5 are refused by name; before, -5 minted total_chunks of -0, "100" was stored as sent and reached Math.ceil("100"/5242880) through Number(totalSize). 0 was already refused (truthiness) under the generic "required" message; it now reaches the parse and is refused as totalSize: Too small… — same status and code, a better message.
  2. chunkSize below 5242880 is refused 400, not clamped — right, and exactly the ruling's named behaviour choice. A numeric-string chunkSize is refused — right (the clamp's Math.max("5242880", 5242880) coerced it). Above the minimum it is honoured unchanged (CONTROL row 10485760 → 2 chunks for 12 000 000 bytes).
  3. Omitted chunkSize keeps the default — right. Zod applies .default(5242880) on undefined, so parsed.data.chunkSize is always a number and Math.ceil(totalSize / chunkSize) for a valid body is what reqChunkSize ?? 5242880 produced before; the CONTROL row pins 12 000 000 → 3 chunks and the stored chunk_size. Every in-repo caller omits it or sends 5242880 (see ④), so no existing session shape moves.
  4. chunkSize: null is refused — right under the declared schema. .default() is not .nullable(); null was formerly read as omitted by ??. This is a narrowing the card did not name but the schema does, and the changeset lists it.
  5. bucket must be a string when sent; null or a non-string is refused — right (z.string().optional()); before, any type was written to both rows' bucket.
  6. metadata must be an object of string values; { tag: 1 } or null is refused — right (z.record(z.string(), z.string()).optional()); before, any shape was JSON.stringify-ed onto both rows and handed to storage.initiateChunkedUpload(key, { contentType, metadata }), whose adapters (S3 user metadata) take string values only — so this makes a real refusal real, not a new rule.
  7. scope — unchanged accept set. The gate requireAcceptedUploadScope(body.scope, res) runs BEFORE the parse, so an off-vocabulary or retired public scope keeps its dedicated remedy text; null was and is refused there (UploadScopeSchema.safeParse(null) fails). The parse then applies the declared default; scope ?? 'user' downstream is now redundant but harmless.
  8. Unknown keys — unchanged: z.object strips them; the handler never read them.
  9. Gate order — presence → scope → parse → 413 limit → createFile → backend multipart → createSession. Right: both rows and the backend multipart sit after the parse (the seven REFUSED rows assert createFile and createSession never called). requireWithinUploadLimit now receives a typed number; the Number(totalSize) coercion that let a numeric string through is gone.
  10. Public surface — no schema, type, export or error code moves. storage.zod.ts is byte-identical to main; the refusal reuses the presigned door's 400 INVALID_REQUEST envelope from fix(service-storage): refuse an out-of-range chunkIndex and a non-byte-count presigned size at the upload doors #27537 with the field path in the message; the success body is unchanged.

② Semver level

  • Changeset .changeset/27549-chunked-init-declared-shape.md: '@objectstack/service-storage': patch; line-initial Clause-②: no (narrowing) and Metadata-protocol: none; BREAKING banner present (**BREAKING** (an accept-set narrowing), shipped as patch); FROM / TO / Fix account present. PR title carries !; PR body carries Clause-②: no (narrowing — …) and Metadata-protocol: none. readClause2Line reads both lines as { kind: 'declared', value: 'no', arm: 'narrowing' } — the arm is the first token inside the parenthesis, so the body's long parenthetical is well-formed.
  • Level — right. Metadata-protocol: none ⇒ patch; a non-protocol breaking change ships patch and keeps its banner and disposition (Post-Task Checklist §3). Only @objectstack/service-storage publishes a change; @objectstack/client and @objectstack/spec are untouched.
  • ADR-0087 disposition — exactly one marker, adr-0087: not-required (no-migration-prescription) …, in an HTML comment. readDisposition parses it (ok: true, category no-migration-prescription); findMigrationPrescription on the body returns null — the bold FROM. / TO. / Fix. paragraphs are AGENTS.md's required migration account for a breaking changeset, not a FROM → TO ledger prescription. Right in substance too: no key of any metadata schema, no export, type or error code is removed, renamed or re-shaped, so objectstack migrate meta has nothing to rewrite and no tombstone is owed. Gate verdict: Check Changeset success.

③ Boundary flags

Dev report open_questions: []. Every flag the dev raised, and the two I add:

  • scope ?? 'user' now redundant (dev, out-of-scope note) — agreed, cosmetic; no action owed.
  • filename: '' / mimeType: '' refused by the pre-existing presence check although z.string() accepts them (dev, Acceptance notes) — a pre-existing declared≠enforced in the STRICTER direction, outside this card; not introduced here. Carrier: none — file-worthy as its own p3 card if the maintainer wants the door to accept exactly the declared set in both directions.
  • null for chunkSize / bucket / metadata now refused (dev) — judged in ①.4–6: right under the declared schema and the ruling.
  • Dev claim "no Console path calls this door" — verified: git grep under ui/ at the head finds no upload/chunked or initChunkedUpload caller.
  • Dev claim "the SDK sends the schema's own input type" — verified: packages/client/src/index.ts:5448 initChunkedUpload(req: InitiateChunkedUploadRequest), where InitiateChunkedUploadRequest is the z.input projection of InitiateChunkedUploadRequestSchema; the body is JSON.stringify(req).
  • PR body Skills-docs: none — no skill or doc teaches the old clamp — does not hold against the tree; see ④ should-fix.
  • Serial constraint: PR fix(service-storage): refuse an out-of-range chunkIndex and a non-byte-count presigned size at the upload doors #27537 is merged and the base already carries its presigned parse; the diff applies cleanly on origin/main (the three-file diff is identical from the merge-base and from main).

④ Design judgment

The narrowing makes the spec's own refusal real; it takes no declared capability away. Every shape the door now refuses is a shape InitiateChunkedUploadRequestSchema on main already declares invalid, and the generated reference (content/docs/references/api/storage.mdx: totalSize integer ✅, chunkSize integer optional default 5242880) already told consumers so. The behaviours removed — the 5 MiB clamp, null-as-omitted, type-agnostic bucket / metadata, a coerced numeric-string total — were undeclared leniencies, and the one with a consumer-visible effect (the clamp) silently produced a totalChunks different from the one the caller computed; the QA checklist records that very trap. It holds against the ruling (parse with the declared schema; refuse, do not clamp) and against the code it cites: the presigned door's #27537 parse is a hand check because ITS schema declares a bare z.number() (part (b), split to domain:spec); the init door's schema is already tight, so a whole-schema parse is the right instrument and the two doors answer the same envelope.

Trying to break it at the consumers: every in-repo caller of the init door sends { filename, mimeType, totalSize: n ≥ 1 } as JSON numbers with chunkSize omitted or exactly 5242880 — storage-routes.test.ts, chunked-upload-integrity.test.ts, chunk-part-record-concurrency.test.ts, storage-limits.test.ts, storage-routes.metadata-outage.test.ts, storage-routes.server-fault-withheld.test.ts, the success/error-envelope conformance tests, sys-file- / sys-upload-session-organization-stamping.test.ts, tenant-audit-update-delete-half-repairs.test.ts, upload-door-ownership.test.ts, upload-scope-vocabulary.test.ts; storage-upload-door-ownership.wall.test.ts and storage-upload-organization-change.wall.test.ts (organizations); attachments-permission-matrix, error-sentences-live, storage-chunked-parallel-parts, storage-chunked-resume-integrity (dogfood); storage-wire-dialect.test.ts (client, mocked fetch). None sends a body the new parse refuses. The SDK's resumeUpload(uploadId, file, chunkSize, resumeToken) takes its chunk size from the caller's own session, not this door. Dogfood Regression Gate and Build Core are green on the head; Test Core shards were still running when read.

Findings:

  • should-fix — docs/qa/platform-checklist/areas/attachments-storage.json still teaches the removed behaviour in two strings of the upload-session lifecycle item: steps[0] ("chunkSize is floored at 5 MiB, so a smaller requested value silently yields a different totalChunks than the caller computed") and source ("chunkSize = Math.max(reqChunkSize ?? 5242880, 5242880), i.e. a 5 MiB floor"). After this PR a runner following that step is told to expect a clamp and gets a 400. Rewrite both to the refusal (and bump the item's revision with a history entry, as check-platform-checklist.mjs requires). Not a must-fix: the checklist is a QA ledger, not a consumer; no gate reads that prose; the storage-routes.ts#mimeType anchor still resolves. May land in this PR or as a follow-up card.
  • must-fix — none.

Implemented-by: claude/project-thread-und9e8
Reviewed-by: session_016WV4d1Np3hU7RQnA13YZcL

VERDICT: PASS

…not the 5 MiB floor

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4d1JR8d3KVqut8R6TWN
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1JR8d3KVqut8R6TWN

Delta for contract review 6510724527's should-fix, new head e77ad275 (one commit on top of 26e6d217):

  • docs/qa/platform-checklist/areas/attachments-storage.json, item attachments-storage.upload-session-abort: steps[0] and the storage-routes.ts#mimeType source line now describe the 400 INVALID_REQUEST refusal (schema parse, no clamp, omitted chunkSize takes the 5242880 default). revision 6 to 7 with a matching history entry. No clause, oracle or variant changed.
  • pnpm check:platform-checklist exit 0 (352 items; symbol anchors 1064/1074 resolved, the same residual as before). check-platform-checklist.mjs --changed-from origin/main exit 0 (9 anchors judged, 0 lost).
  • The commit added four derived families: check-adr-links and its self-test, check:doc-formula-expressions and check:platform-checklist. All four exit 0, as do re-runs of check:nul-bytes, check-affected-docs and check:issue-citations. --ran reports 68 derived, 68 run, 0 NOT-MEASURED. The other 64 families ran on 26e6d217; the new commit touches only this JSON file.

PR body correction (a dev does not re-write the PR body, so it is recorded here): the Skills-docs: line should read Skills-docs: updated here — docs/qa/platform-checklist/areas/attachments-storage.json (the upload-session item's init step and source line taught the removed 5 MiB clamp). The none line in the body does not hold, as the review found.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract-review-claim: session_016WV4d1Np3hU7RQnA13YZcL · head e77ad27 · 2026-10-29T23:20Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: e77ad2750539a20766866f78c526b8f78b9d2516
Local-runs: none
Scope: delta 26e6d217..e77ad275 — docs/qa/platform-checklist/areas/attachments-storage.json (item attachments-storage.upload-session-abort: steps[0], one source line, revision 6→7, one history entry) + the dev's Skills-docs: correction (PR comment 6510825689), the init handler in packages/services/service-storage/src/storage-routes.ts at this head (the code the rewritten strings describe), scripts/check-platform-checklist.mjs and docs/qa/platform-checklist/README.md (the invariants the edit must keep), skills/** and content/docs/** (searched for anything teaching the init door), and .claude/skills/pm-dispatch/SKILL.md (the Skills-docs: menu). The three files of 26e6d217 are unchanged at this head and stand reviewed by record 6510724527 (PASS); this record is the delta's.

Read-only shape held: the delta diff, the card's and the PR's comments since that record, the head's check-runs. Nothing built, run or re-run. One head-tree JSON was parsed offline to read two numbers (the item's revision and its last history entry's) — a read, not a gate run.

Check-runs on e77ad275 as read at 2026-10-29T23:25Z: success — Auto Label, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate, Flag docs affected by code changes, Governed Surface Queue Guard, Lint · ESLint, Lint · PM dispatch-gates, Lint · docs & code guards, Lint · repo & tooling gates, Lint · scoped · query-options erasure, Lint · scoped · tooling self-tests, PR-body guards, Plan Test Core shards, Test UI, Type Check · debt ledger, Type Check · source gates, filter. In progress — Build Core, Lint · CI/release/engine, Lint · scoped · slot lookup & corpus, Live E2E UI, Test Core 1/8 … 8/8, Type Check · consumer gates, Type Check · workspace. No failure read. ⚠️ check:platform-checklist is kept OFF per-PR CI by the standing maintainer decision recorded in lint.yml (the watchdog in fleet-patrols.yml runs it against main), so NO check-run on this head is that gate's verdict; its two invariants are judged by reading in ④, and the dev's local exit 0 is a dev claim only. The Tier S landing waits for every in-progress run above.

① Derived judgments

The delta touches one JSON ledger under docs/qa/ and nothing under packages/**, skills/** or content/docs/**. It implies no accept-set change and no public-surface change: no schema, route, type, export, error code or published package moves. The rewritten strings are judged for truth against the head's door in ④.

② Semver level

Unchanged by the delta: no published package is touched, the changeset .changeset/27549-chunked-init-declared-shape.md is byte-identical to 26e6d217 (patch, Clause-②: no (narrowing), Metadata-protocol: none, BREAKING banner, one ADR-0087 not-required (no-migration-prescription) marker), and the PR body's declaration lines are unchanged. Record 6510724527's ② stands. Check Changeset is success on this head.

③ Boundary flags

  • Skills-docs: correction (comment 6510825689). The dev proposes Skills-docs: updated here — docs/qa/platform-checklist/areas/attachments-storage.json (the upload-session item's init step and source line taught the removed 5 MiB clamp). Right spelling and right value. The menu is updated here | follow-up owed — FILES | follow-up #N | none — REASON (.claude/skills/pm-dispatch/SKILL.md, the claim-template notes), and the in-stock changesets spell the first arm exactly as updated here — FILES (26939, 27462). none was false because one file in the tree taught the removed behaviour; that file is rewritten in this diff, so updated here — that file is the truthful arm. One addition I would make, following the stock spellings (26939, 27506): carry the skills half too — no file under skills/** teaches POST …/upload/chunked, chunkSize or InitiateChunkedUpload (the only chunkSize hit under skills/ is an unrelated must_not_contain in skills/objectstack-ai/evals/skills-tools-knowledge.json), and no hand-written content/docs/** page names chunkSize; the generated content/docs/references/api/storage.mdx already states the schema the door now enforces, so no skill line changes.
  • The PR body itself still reads Skills-docs: none — …, which does not hold; the correction lives only in a comment because the dev does not rewrite the PR body. The review checklist wants that line present AND true in the body — this is a seat act owed before ACCEPT: rewrite the body's line to the corrected value (post-stamped.mjs --body=27781). Named below as a should-fix on the PR body, not on the tree.
  • Dev claims in 6510825689 — pnpm check:platform-checklist exit 0, --changed-from origin/main 0 anchors lost, four added gate families green — are dev claims; the two mechanical invariants are re-derived by reading in ④, the rest has no check-run on this head (see the ⚠️ above) and is not relied on.
  • Claim line for this head (card comment 6510844066) names this seat session and this head.

④ Design judgment

The delta closes record 6510724527's one should-fix, and the rewritten text holds against the code it describes at this head:

  • steps[0] now says the body is parsed with InitiateChunkedUploadRequestSchema; a totalSize that is not a whole JSON number of at least 1, or a chunkSize below 5242880, is refused 400 INVALID_REQUEST naming the field, never clamped; an omitted chunkSize takes the declared 5242880 default. Each clause matches the handler: InitiateChunkedUploadRequestSchema.safeParse(body) → sendError(res, 400, 'INVALID_REQUEST', chunkedInitRefusalMessage(issues)) with path: message per issue; the schema's .int().min(1) / .int().min(5242880).default(5242880); the retained "required trio" and "size spelling refused" clauses are still true (the presence check is unchanged). Right.
  • The source line keeps its anchor packages/services/service-storage/src/storage-routes.ts#mimeType (still resolves — mimeType is read in the handler) and now describes the parse and the refusal instead of Math.max(reqChunkSize ?? 5242880, 5242880). Right.
  • Gate invariants (check-platform-checklist.mjs, README "Change"): revision is 7 and the last history entry's revision is 7 (parsed from the head tree); the entry carries { revision, date, change, ref }, the same keys as every earlier entry; revisions run 1…7 monotonic; the change text states what changed and that no clause, oracle or variant changed — a semantic edit to prose with a bumped revision and an appended row, exactly the README's procedure; the row stays, nothing is deleted. Right. history[].ref is #27549, the card — consistent with earlier rows citing their cards.
  • Trying to break it: a search of the head's checklist files for floor, clamp and Math.max finds the removed behaviour only in two history rows of this item (revision 2, which introduced the floor text, and the new revision 7, which says what was replaced) — history describing the past, correctly kept under the ledger's append-only rule; no live steps, source, clause or knownGaps string in any area file still teaches the clamp. The knownGaps and automated.ref blocks are untouched and still true.

Findings:

  • should-fix (PR body, seat act, not the tree) — the PR body's Skills-docs: none — … line is false at this head; rewrite it to the corrected updated here — docs/qa/platform-checklist/areas/attachments-storage.json (…) value, ideally with the skills half above, before ACCEPT. The tree needs no further change for it.
  • must-fix — none.

Implemented-by: claude/project-thread-und9e8
Reviewed-by: session_016WV4d1Np3hU7RQnA13YZcL

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-elon-musk · session_016WV4d1Np3hU7RQnA13YZcL

维护者速读

  • 改了什么:分片上传初始化接口 POST /api/v1/storage/upload/chunked 现在按已声明的请求 schema 校验请求体:负数或字符串的 totalSize、小于 5 MiB 的 chunkSize 都答 400,不再静默接收或悄悄抬到 5 MiB。
  • 为什么改:声明了却不执行,totalSize: -5 会生成一个永远完成不了的会话(storage: the chunked-upload init door does not enforce its declared totalSize/chunkSize, and the presigned request schema declares size as any number #27549 第 a 部分;第 b 部分已拆给 spec 车道)。
  • 风险与回滚:收窄改动(BREAKING,patch);省略 chunkSize 仍用默认 5 MiB;仓内所有调用方的请求体都不受影响(复审逐个查过)。回滚即还原本 PR。
  • 先看:packages/services/service-storage/src/storage-routes.ts 的 chunked init 处理。
  • 两分钟验证:storage-routes.test.ts 新增用例,去掉解析即 12 例转红。
  • 需要你做的:无(非受管 PR,契约复审 PASS,席位按本任落地授权落地)。

席位意见:ACCEPT。

席位核对

Head-sha: e77ad2750539a20766866f78c526b8f78b9d2516

  • Shape: the init door parses its body with InitiateChunkedUploadRequestSchema (the presigned door's parse-and-refuse shape from PR fix(service-storage): refuse an out-of-range chunkIndex and a non-byte-count presigned size at the upload doors #27537); pins in storage-routes.test.ts; the QA checklist item upload-session-abort (docs/qa/platform-checklist/areas/attachments-storage.json) rewritten to the refusal, revision 6→7; changeset patch @objectstack/service-storage, BREAKING banner, ADR-0087 not-required (no-migration-prescription). check-governed-merges --pr 27781: NOT governed, 127 changed lines.
  • Premise measured: ablation (drop the parse) → 12 red, restore → 70 green; package 1297/1297; dogfood 2867 passed / 0 failed (dev report 6510543998 on storage: the chunked-upload init door does not enforce its declared totalSize/chunkSize, and the presigned request schema declares size as any number #27549).
  • Gate check-runs on the head: 49 runs, 36 success, 13 skipped, 0 failure/pending. check:platform-checklist is not a per-PR CI job by standing decision; the dev's local exit 0 and the delta review's reading of the two invariants stand in.
  • Contract review: CONTRACT_REVIEW_TIER PASS 6510724527 on 26e6d217 and delta PASS 6510908948 on this head (narrowing face, no must-fix).
  • Accepted deviations: the whole-body parse also enforces bucket (string), metadata (string values) and refuses null on the optional keys — all as declared; listed in the changeset.
  • Skills-docs correction: the PR body's Skills-docs: none — … line is superseded (the relay cannot re-edit a PR body). The right value, per the delta review: Skills-docs: updated here — docs/qa/platform-checklist/areas/attachments-storage.json; no skills/** file or hand-written content/docs page teaches the init door's chunkSize, and the generated reference already states the schema (dev comment 6510825689).
  • Out of scope: the door's pre-existing presence check refuses filename: '' / mimeType: '' although the schema's z.string() accepts them (stricter than declared) — filed separately by the seat for triage.
  • Landing: under the maintainer's landing grant for this seat term (seat 4 thread, 2026-10-29 22:04Z): pr_ready + automerge_enable through the relay.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

storage: the chunked-upload init door does not enforce its declared totalSize/chunkSize, and the presigned request schema declares size as any number

1 participant