Skip to content

fix(spec)!: the chunked-upload init request declares filename and mimeType non-empty - #27887

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-27801-chunked-init-filename-min1
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-27801-chunked-init-filename-min1

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by 田騏 · project thread

Fixes #27801

Clause-②: no (narrowing)
Metadata-protocol: none

维护者速读

  • 改了什么:分块上传初始化请求的 schema(InitiateChunkedUploadRequestSchema)现在声明 filename、mimeType 不能为空(min(1)),参考文档同步标注“non-empty”。
  • 为什么改:上传入口一直拒收空文件名、空 MIME 类型(400 INVALID_REQUEST),但公开 schema 写的是“任意字符串”,按 schema 写代码的调用方或 AI 会被误导;现在声明与执行一致。
  • 风险与回滚:上传入口的行为一字未改(状态码、错误码、文案、与 scope 检查的先后都不变),只有直接用这个 schema 解析空值的代码会从通过变成失败;回滚即 revert 本 PR。
  • 先看:packages/spec/src/api/storage.zod.ts(两个 min(1))、packages/services/service-storage/src/storage-routes.ts(入口的存在性检查为何保留的注释)、.changeset/27801-chunked-init-filename-min1.md(BREAKING 说明与 ADR-0087 处置)。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/api/storage.test.ts,预期 65 个测试全过,其中包括“refuses an empty filename / mimeType”两条。
  • 需要你做的:无,席位落地(等契约复审记录)。

Independent review: none run (quota); the PR waits for the seat's contract-review hand-off.

What changes

  • InitiateChunkedUploadRequestSchema.filename and .mimeType declare min(1), and their descriptions read "Original filename (non-empty)" and "File MIME type (non-empty)". The published JSON Schema now carries minLength: 1 on both keys, and content/docs/references/api/storage.mdx is regenerated (2 lines).
  • The door (POST /api/v1/storage/upload/chunked) is unchanged in behaviour. Its presence check !body.filename || !body.mimeType || body.totalSize == null stays where it is. A comment now says that it refuses nothing the schema parse accepts.
  • Changeset 27801-chunked-init-filename-min1.md, @objectstack/spec patch, fix(spec)!, with both declaration lines and the ADR-0087 disposition not-required (no-migration-prescription). This follows the precedent of the other request-declaration narrowings (26817-http-find-query-params-count-select.md, 27683-search-fields-names-a-field.md).

What happens to the door's truthiness check. I kept it, for three measured reasons:

  1. With min(1) the check is a strict subset of the schema's refusal set. A missing, '', null, 0 or false value fails z.string().min(1) as well, so the two never disagree about what is refused.
  2. It runs before the scope gate. The scope gate runs before the parse, and finding(service-storage): an upload start naming a scope outside the sys_file vocabulary answers 500 INTERNAL (an engine invalid_option relayed as an internal fault) instead of a 400 naming the allowed scopes #22470 asked for that order. If the check were folded into the parse, an empty filename sent with an off-vocabulary scope would get the scope refusal instead of the presence refusal. That is the ordering change the order forbids.
  3. Folding it in would also change the message for an empty or missing filename / mimeType from filename, mimeType, and totalSize are required to chunkedInitRefusalMessage's text. A missing totalSize would still need its own check ahead of the scope gate. Keeping the check means status, code, message and ordering stay byte-identical, which is what the ruling's "no behaviour change at the door" asks for.

Tests

  • packages/spec/src/api/storage.test.ts: refuses an empty %s, naming that key covers filename and mimeType. Each asserts success: false, exactly one issue, path equal to the key, and code equal to too_small. CONTROL: a one-character filename and mimeType parse asserts that 'a' / 'b' parse.
  • packages/services/service-storage/src/storage-routes.test.ts: REFUSED 400 INVALID_REQUEST: an empty %s, which the declared schema refuses too, with no row written covers filename and mimeType. Each asserts status 400, success: false and error.code INVALID_REQUEST, that InitiateChunkedUploadRequestSchema.safeParse refuses the same body, and that neither createFile nor createSession was called. No earlier test sent an empty filename or mimeType to this door. The existing chunked-init cases cover totalSize, chunkSize and metadata only.
Ablation (spec): removing each min(1) turns its own case red

Run with node scripts/ablation-replace.mjs. The anchor hit once each time and the file was restored to its HEAD blob (e4b1a7313842), with git diff HEAD empty.

  • filename: z.string().min(1) replaced by filename: z.string(): × refuses an empty filename, naming that key, Tests 1 failed / 64 passed (65).
  • mimeType: z.string().min(1) replaced by mimeType: z.string(): × refuses an empty mimeType, naming that key, Tests 1 failed / 64 passed (65).

The door test is not ablated, because the door's behaviour did not change. Its schema-agreement assertion reads @objectstack/spec/api through the built dist/.

Local runs
  • pnpm --filter @objectstack/spec exec vitest run src/api/storage.test.ts: Test Files 1 passed, Tests 65 passed.
  • pnpm --filter @objectstack/service-storage test: Test Files 71 passed (71), Tests 1311 passed (1311). pnpm --filter @objectstack/service-storage typecheck: exit 0, and check:test-typecheck OK.
  • pnpm --filter @objectstack/spec check:generated: 1 of 14 stale (content/docs/references/**) before --fix. --fix regenerated gen:docs and re-checked green. After merging origin/main, the spec was rebuilt and all 14 generated artefacts are up to date.
  • Probe on the merged head, driving both doors and both schemas with '':
    • presigned filename '': schema success: true, door 400 INVALID_REQUEST.
    • chunked filename '': schema success: false, door 400 INVALID_REQUEST.
    • The same holds for mimeType.

Acceptance notes

Verification

All readings below are on head f5bb94f94e, which is the branch merged with origin/main at 755e28500.

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 109 commands, and all 109 were run with each exit code captured before any pipe.
    • 106 exited 0 on the first pass.
    • 3 exited 3 (PREREQUISITE NOT MET, unbuilt dist/): check:skill-examples, check:dual-build-cjs-loads and check:i18n. All 3 exited 0 after a workspace build.
    • --ran reconciliation: "109 derived, 109 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero — all 109 recorded an exit code and none of them is 3)".
  • node scripts/check-adr-0087-registration.mjs --base origin/main exited 0 and reads the changeset as [BREAKING+bang+clause-②-narrowing] not-required (no-migration-prescription).
  • Full dogfood (pnpm --filter @objectstack/dogfood test, under the lock, closure built) gave VERDICT command-exit 1: Test Files 3 failed / 352 passed / 3 skipped (358), Tests 4 failed / 2864 passed / 20 skipped (2888). All 4 failures still fail when their files are run alone, and none touches storage:
    • 2 in error-sentences-live.dogfood.test.ts and 1 in object-hook-lifecycle-scratch.dogfood.test.ts assert that the /data door answers an undeclared hook abort with no code. Commit 21c4bdd804 on main made that answer HOOK_ABORTED.
    • 1 in platform-app-object-entry-views.test.ts names sys_exchange_rate as undeclared. That object arrived on main with 194e66c028.
    • This is main-red on the push-only battery, reported to the seat and not caused by this diff.
  • CI check-runs: not yet read. The PR opens as a draft, and the seat reads convergence.

🤖 Generated with Claude Code

https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V

…e non-empty

InitiateChunkedUploadRequestSchema.filename and .mimeType carry min(1),
so the declaration says what the init door has always refused. The
door keeps its presence check ahead of the scope gate: status, code and
message are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…unked-init-filename-min1

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-storage, @objectstack/spec, touching 5 documentable anchor(s).

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

  • content/docs/api/plugin-endpoints.mdx (via initChunkedUpload (sdk, the bare tail of client method storage.initChunkedUpload, bound to POST /api/v1/storage/upload/chunked), storage.initChunkedUpload (sdk, the route ledger binds it to POST /api/v1/storage/upload/chunked))
  • content/docs/kernel/runtime-services/storage-service.mdx (via initChunkedUpload (sdk, the bare tail of client method storage.initChunkedUpload, bound to POST /api/v1/storage/upload/chunked))
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 — 169 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 da94c11337fdb4c37ee809e9748a76791a5199cc → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d5745438bab10523288d43fd94de3e7af521416d — the merge of head f5bb94f94e92b0929e3c415f692e4e50cbff8308 into base da94c11337fdb4c37ee809e9748a76791a5199cc, 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 d5745438bab10523288d43fd94de3e7af521416d && git checkout d5745438bab10523288d43fd94de3e7af521416d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin da94c11337fdb4c37ee809e9748a76791a5199cc f5bb94f94e92b0929e3c415f692e4e50cbff8308 && git checkout -B drift-repro da94c11337fdb4c37ee809e9748a76791a5199cc && git merge --no-ff f5bb94f94e92b0929e3c415f692e4e50cbff8308

node scripts/docs-audit/affected-docs.mjs --json da94c11337fdb4c37ee809e9748a76791a5199cc

⚠️ 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 da94c11337fdb4c37ee809e9748a76791a5199cc → 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:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V

席位核对 · ACCEPT (lane part) · card #27801 · 2026-10-30T02:43Z

  • Shape: 6 files, +74 / −4 at head f5bb94f9. InitiateChunkedUploadRequestSchema.filename / .mimeType declare min(1), describe text "(non-empty)" (packages/spec/src/api/storage.zod.ts:197–200); content/docs/references/api/storage.mdx regenerated (2 rows). Spec test: '' refused per key, one-character control parses. Door test: an empty filename and an empty mimeType each answer 400 INVALID_REQUEST, the schema refuses the same body, no row written. Changeset @objectstack/spec patch, fix(spec)!, Clause-②: no (narrowing) / Metadata-protocol: none, ADR-0087 not-required (no-migration-prescription) (precedent: the 26817 and 27683 changesets). Matches triage 6511379936 ("declare min(1) … Contract first; no behaviour change at the door").
  • The door: the truthiness check at storage-routes.ts:1122 is kept byte-identical, with one comment added. With min(1) it refuses nothing the schema accepts. It runs ahead of the scope gate, so folding it into the parse would change the answer for an empty filename sent with an off-vocabulary scope, and would change the message. Status, code, message and ordering are unchanged.
  • Dev: opus under the quota-exhaustion exemption (claim on this card). Ablation: dropping min(1) from either key reds exactly that key's spec test. service-storage 1311/1311 and typecheck green; gates 109/109 exit 0, --ran 0 unmeasured. os-dev-report 6513641996. CI was in progress when this was posted, with no failures.
  • Acceptance notes:
  • Owed before any landing stroke: the CONTRACT_REVIEW_TIER review (a packages/spec/src/** hunk plus a (narrowing) arm); this seat cannot serve that tier now and hands it over below.

Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 "You've reached your Fable limit", 2026-10-29T17:05Z) · head f5bb94f

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head f5bb94f94e92b0929e3c415f692e4e50cbff8308 · 2026-10-30T03:01Z


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f5bb94f94e92b0929e3c415f692e4e50cbff8308
Local-runs: none
Scope: packages/spec/src/api/storage.zod.ts + packages/services/service-storage/src/storage-routes.ts, packages/services/service-storage/src/storage-routes.test.ts, packages/spec/src/api/storage.test.ts, .changeset/27801-chunked-init-filename-min1.md, content/docs/references/api/storage.mdx, packages/client/src/index.ts

Inputs read: card #27801 (body, triage 6511379936, claim 6512389552, os-dev-report 6513641996), PR #27887 (body, 6 files, net diff against main at da94c113), the lane's ACCEPT 6513663903 and hand-off, and the 55 check-runs on the head: every run completed, each success or skipped (path-filtered matrix legs), none failure — Test Core 8/8 shards, TypeScript Type Check (the job that holds every spec-artefact gate), Lint & Repo Gates, Check Changeset (the job that runs the ADR-0087 registration gate), Governed Surface Queue Guard, PR-body guards, Spec property liveness, Dogfood Regression Gate, Build & E2E UI, Live E2E UI all green. The cited code was read in the live tree at the head; nothing was built, run or re-run.

① Derived judgments

  • Accept-set narrowing, InitiateChunkedUploadRequestSchema.filename and .mimeType (storage.zod.ts:197-200): bare z.string() becomes z.string().min(1); a parse of '' on either key now fails with one too_small issue on that key. This is exactly the ruling 6511379936 graded ("declare min(1) … Contract first; no behaviour change at the door"). Right.
  • Who sends an empty filename or mimeType today, and what they get after. Every in-repo sender was read: the SDK storage.initChunkedUpload (packages/client/src/index.ts:5448) serialises the caller's request without parsing it and has no in-repo caller that builds a body; the client wire-dialect test (big.bin), the two organizations wall tests (wall-chunked.bin), the storage-routes tests and the QA checklist all send non-empty values. The three filename: '' senders in the tree (service-print, field-file-service, file-attachment-service tests) drive other services and never this schema. So no caller loses a working path. A caller that did send '' over the wire got 400 INVALID_REQUEST filename, mimeType, and totalSize are required from the door's truthiness check before this PR and gets the identical answer after; a caller that parsed such a body directly with the schema got success: true before and gets a too_small refusal on that key after. The narrowing takes no declared capability away at the consumer that the door honoured; it makes the door's refusal the spec's own. Right.
  • Public TypeScript surface unchanged. InitiateChunkedUploadRequest is z.input and stays string for both keys; InitiateChunkedUploadRequestParsed likewise; no export is added, removed or renamed, so check:api-surface is unaffected (TypeScript Type Check green). Right.
  • Door behaviour unchanged (storage-routes.ts:1118-1137): the truthiness check stays first, the #22470 scope gate second, the #27549 schema parse third. With min(1) the set the truthiness check refuses for these two keys (missing, '', null, 0, false) is a strict subset of what the parse refuses (all of those are not a non-empty string), so declared and enforced now agree on every value of filename and mimeType; status, code, message and ordering are byte-identical. The diff adds one comment and no code at the door. Right.
  • Whitespace-only values remain admitted by both the schema and the door; the two agree, and the ruling did not ask for more. Right to leave alone.
  • Generated artefacts. The only checked-in artefact carrying .describe() text is content/docs/references/api/storage.mdx (2 rows regenerated); declaration-map, json-schema.manifest, authorable-surface and authorable-defaults list names and defaults only and are untouched by a min(1) — nothing stale (TypeScript Type Check green). Right.
  • Nothing widened; no sibling touched. GetPresignedUrlRequestSchema.filename / .mimeType (storage.zod.ts:67-68) keep the same declared-versus-enforced gap against the presigned door (storage-routes.ts:968); the lane routed it to storage: GetPresignedUrlRequestSchema.size declares no lower bound; narrow it to int().min(0) (#27549 part b) #27598, the open card on that schema. Out of this card's scope, correctly not folded in. Right.

② Semver level

Changeset .changeset/27801-chunked-init-filename-min1.md: @objectstack/spec patch, fix(spec)!, line-initial Clause-②: no (narrowing) and Metadata-protocol: none, BREAKING banner, Before / Now / Unchanged / Who sees it / remedy, and exactly one ADR-0087 marker not-required (no-migration-prescription). This is the level the house rule assigns a non-protocol narrowing (patch with its BREAKING banner and disposition) and matches the two precedents it names (26817-http-find-query-params-count-select, 27683-search-fields-names-a-field, both on main). Metadata-protocol: none is right: a REST request body is not something a metadata author writes. The disposition is right: no key is added, removed, renamed or re-shaped, no stored shape moves, and the body carries a remedy for a caller, not a rewrite prescription — the gate's statement-against-statement check passed (Check Changeset green). @objectstack/service-storage receives a comment and a test only, which publish nothing, so no second changeset is owed. The PR body carries the same two declaration lines.

Clause-②: no (narrowing) — matches what the diff publishes.

③ Boundary flags

④ Design judgment

packages/spec/src/api/storage.zod.ts — judged against the ruling it implements (triage 6511379936) and its first consumer, the POST /api/v1/storage/upload/chunked door (storage-routes.ts:1113-1137), plus the SDK method that carries its input type.

  • Against the ruling. Two min(1) calls and two descriptions reading "(non-empty)"; nothing else in the schema moves. Holds.
  • Against the door. I tried to find a value on which the two still disagree. '': truthiness refuses, parse refuses. ' ': both admit. 0, false, null, missing: truthiness refuses, parse refuses (not a string). A non-empty string: both admit. No value remains that one side admits and the other refuses, so the card's filing gate ("declared and enforced disagree on a published request schema") is closed for these two keys. The door is reached first by the truthiness check, so the parse's too_small text is never the wire answer for '' — the door's status, code and message are unchanged, which is what the ruling asked for; the new door comment states the subset relation accurately (it also holds for totalSize == null against z.number()). Folding the check into the parse would have reordered an empty filename with an off-vocabulary scope against the #22470 scope gate and changed the message — the dev's reason for keeping it is the correct reading of the tree.
  • Against the SDK. storage.initChunkedUpload(req: InitiateChunkedUploadRequest) keeps its signature (string stays string), does not parse client-side, and has no in-repo caller that builds a body. No consumer loses a capability.
  • Tests, read not run. Spec: '' per key gives success: false, exactly one issue, path [key], code too_small (the zod 4 code for min), with a one-character control that parses — the ablation account (each min(1) reds its own case) is consistent with this shape. Door: '' per key gives 400 INVALID_REQUEST, the schema refuses the same body, and neither createFile nor createSession is called; no earlier door test sent an empty value. Both land in the existing describe blocks and import from the same @objectstack/spec/api entry the door itself imports.
  • The narrowing itself. It removes nothing a consumer could use (the door already refused '') and makes the spec's own refusal real on the one surface — direct schema parse, generated clients, the reference doc — where the wider declaration misled. The sibling presigned schema keeps its gap; that is a separate card, not a defect of this change.

Findings: no must-fix. No should-fix on the hits file.

Implemented-by: claude/issue-27801-chunked-init-filename-min1
Reviewed-by: session_01L6bhgV4fMGJHSdSdx3u9mK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 03:08
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 03:08
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit a68430a Oct 30, 2026
59 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-27801-chunked-init-filename-min1 branch October 30, 2026 03:34
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/s tests tooling

Projects

None yet

1 participant