Skip to content

fix(spec)!: the presigned upload request declares a whole, non-negative size and a non-empty filename and mimeType - #28136

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

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-f1kiqr

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm

Requested by Tesla · project thread

Fixes #27598

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

维护者速读

  • 改了什么:预签名上传请求的 schema(GetPresignedUrlRequestSchema)现在声明 size 必须是非负整数(int().min(0)),filename、mimeType 不能为空(min(1));上传入口同时开始拒收小数字节数(400 INVALID_REQUEST)。
  • 为什么改:公开 schema 原来接受负数、小数和空字符串,入口却拒收负数和空值,按 schema 写代码的调用方或 AI 会被误导;小数字节数更糟,入口会发出上传地址并写一条永远无法完成的待上传记录。
  • 风险与回滚:负数、非数字、空文件名/类型的入口行为一字未改;唯一的行为变化是小数 size 从 200 变成 400,正常客户端发的 File.size 本来就是整数。回滚即 revert 本 PR。
  • 先看:packages/spec/src/api/storage.zod.ts(三处收窄)、packages/services/service-storage/src/storage-routes.ts(isDeclaredByteCount 改用 Number.isInteger)、.changeset/27598-presigned-size-whole-non-negative.md(BREAKING 说明与 ADR-0087 处置)。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/api/storage.test.ts,预期 73 个测试全过,其中包括 “refuses a fractional size, naming size”。
  • 需要你做的:无,席位落地(等契约复审记录)。

What changes

  • GetPresignedUrlRequestSchema.size is z.number().int().min(0), and its description reads "File size in bytes: a non-negative whole number (0 is an empty file)". The published JSON Schema now types it integer with minimum: 0.
  • Declared increment (same file, same declared-versus-enforced class, the fix(spec)!: the chunked-upload init request declares filename and mimeType non-empty #27887 precedent): GetPresignedUrlRequestSchema.filename and .mimeType declare min(1), described "(non-empty)". The order allowed this only if the door refuses '' today. Measured on main 2e955dde: the door's presence check !filename || !mimeType || size == null (storage-routes.ts:968) answers 400 INVALID_REQUEST for an empty value of either key. The new door test below pins that reading.
  • Door (POST /api/v1/storage/upload/presigned): isDeclaredByteCount asks Number.isInteger(size) in place of Number.isFinite(size), so a fractional size is refused 400 INVALID_REQUEST before any sys_file row is written. The refusal message now says "a non-negative whole number". Measured on main 2e955dde: size: 1.5 answered 200, minted an upload URL and wrote a pending row. That row could never commit, because the complete door compares the held byte count (always whole) with the declared one (heldBytesRefusal, storage-routes.ts). Without this change the narrowed declaration would refuse a value the door still accepts, which is the same declared-versus-enforced gap in the other direction.
  • content/docs/references/api/storage.mdx is regenerated (3 lines).
  • Changeset 27598-presigned-size-whole-non-negative.md: @objectstack/spec patch and @objectstack/service-storage patch (the declared kind is none: a REST request declaration that no metadata type embeds is not authorable metadata, the seat's claim correction 6524182832 on storage: GetPresignedUrlRequestSchema.size declares no lower bound; narrow it to int().min(0) (#27549 part b) #27598), fix(spec)!, both declaration lines, and the ADR-0087 disposition not-required (no-migration-prescription).

The door still does not parse its body with the schema. Its presence check and byte-count check run before the scope gate, as #22470 requires, so status, code, message and ordering stay as they were for every value other than a fraction.

Tests

  • packages/spec/src/api/storage.test.ts:
    • refuses %s, naming size covers a negative size, a fractional size and a negative fraction. Each asserts success: false, exactly one issue, and path equal to ['size'].
    • refuses an empty %s, naming that key covers filename and mimeType, with code too_small.
    • CONTROL: size %d parses (0 is an empty file) covers 0, 1 and 1048576.
  • packages/services/service-storage/src/storage-routes.test.ts:
Ablations: each narrowing turns its own case red

Each run used node scripts/ablation-replace.mjs. The anchor hit once, and the file was restored to its HEAD blob with git diff HEAD empty.

  • size: z.number().int().min(0) changed to z.number().min(0): × refuses a fractional size, naming size, 1 failed / 72 passed (73).
  • .min(0) removed from size: × refuses a negative size, naming size, 1 failed / 72 passed (73). The negative fraction stays refused by int().
  • filename min(1) removed: × refuses an empty filename, naming that key, 1 failed / 72 passed (73).
  • mimeType min(1) removed: × refuses an empty mimeType, naming that key, 1 failed / 72 passed (73).
  • Door Number.isInteger(size) changed back to Number.isFinite(size): × REFUSED 400 INVALID_REQUEST: size as a positive fraction, with no sys_file row written, 1 failed / 74 passed (75). Before the door change, the same case failed with the door's 200 body, which is the measurement of main quoted above.
Local runs

The package runs below were taken on bbd24a5f / 6e328c49 (before the merge with origin/main). The storage suites were re-run on the final head 902d10c0: spec storage.test.ts Tests 73 passed (73); @objectstack/service-storage Test Files 71 passed (71), Tests 1314 passed (1314).

  • pnpm --filter @objectstack/spec test: Test Files 756 passed (756), Tests 22518 passed, 1 todo. pnpm --filter @objectstack/spec typecheck: exit 0.
  • pnpm --filter @objectstack/service-storage test: Test Files 71 passed (71), Tests 1314 passed (1314). 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. After --fix all 14 are up to date.
  • node scripts/check-adr-0087-registration.mjs: 1 declared-breaking changeset, carrying not-required (no-migration-prescription).

Acceptance notes

  • Out of scope, reported to the seat and not filed: the SDK's storage.upload (packages/client/src/index.ts) sends mimeType: file.type. A browser File of unknown type has type === '', and the presigned door refuses that with 400 INVALID_REQUEST, so such a file cannot be uploaded through the SDK helper. This PR now also declares that refusal. The fix belongs in the client (send application/octet-stream), not in the door. Not measured end to end in a browser.
  • A whitespace-only filename / mimeType is still admitted by both the schema and the door. The two agree, and this PR leaves that alone, as fix(spec)!: the chunked-upload init request declares filename and mimeType non-empty #27887 did.
  • The claim writes Clause-②: no. The order and triage 6500594085 write no (narrowing), and check-changeset-no-major treats narrowed beside a line without the (narrowing) arm as a contradiction. This body and the changeset carry no (narrowing).

Verification

All readings below are on head 902d10c0, the branch merged with origin/main at 8cb8c7f461, after a full pnpm turbo run build (116/116 tasks).

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 109 commands (the same 109 before and after the merge). All 109 were run with stdin from /dev/null and the exit code captured before any pipe: 109 exited 0. On the pre-merge head, 4 first refused with exit 3 (PREREQUISITE NOT MET: no workspace build, and one self-test that needs an unshallow clone); after the build and git fetch --unshallow they exited 0.
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran FILE: "109 derived famil(ies) accounted for — 109 run, 0 NOT-MEASURED".
  • pnpm --filter @objectstack/spec check:generated on the merged head: all 14 generated artefacts up to date.
  • Full dogfood (owed by the (narrowing) arm), pnpm --filter @objectstack/dogfood test under the verify lock on 902d10c0: Test Files 1 failed | 357 passed | 3 skipped (361), Tests 1 failed | 2882 passed | 20 skipped (2903). The one failure is not this change. test/api-action-door-forward.dogfood.test.ts > "the switch holds: ban_user OFF answers 409 ACTION_DISABLED through the door and the user is NOT banned" expects the ACTION_DISABLED message to contain sys_metadata_activation, and the message is now the plain sentence "This action is turned off for this deployment. ..." (packages/objectql/src/action-activation.ts). Both the test and the new sentence are already on origin/main, and this PR touches neither file nor anything they import from storage. Reported to the seat as a main-red candidate.
  • CI on this PR: not yet read; the seat reads the check-runs.

Generated by Claude Code

…ve size and a non-empty filename and mimeType

GetPresignedUrlRequestSchema.size becomes int().min(0), and filename /
mimeType take min(1), the accept set POST /api/v1/storage/upload/presigned
enforces. The door's byte-count check now also refuses a fractional size,
which minted a URL and a pending sys_file row no upload could complete.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L9qibvsQx1XxK9FrejoXno
…igned request narrowing

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

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/service-storage, @objectstack/spec, touching 6 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 getPresignedUrl (sdk, the bare tail of client method storage.getPresignedUrl, bound to POST /api/v1/storage/upload/presigned), storage.getPresignedUrl (sdk, the route ledger binds it to POST /api/v1/storage/upload/presigned))
  • content/docs/kernel/runtime-services/storage-service.mdx (via getPresignedUrl (sdk, the bare tail of client method storage.getPresignedUrl, bound to POST /api/v1/storage/upload/presigned))
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 — 170 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 d6c0a75f1a4ebdc6c4cf81865226bf72db9f29f6 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 d6c0a75f1a4ebdc6c4cf81865226bf72db9f29f6 → 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#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm

Contract-review-claim: session_01L9qibwb6hBqszPTYJobitm · head 4293804621b82769ea8a42241aeb0c5b833c20ca · 2026-10-30T14:21Z

In-seat at CONTRACT_REVIEW_TIER, per the maintainer: 「你有 fable,可以自己契约复审」.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 4293804621b82769ea8a42241aeb0c5b833c20ca
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

Inputs read: card #27598 (body and all 5 comments, the seat's claim correction 6524182832 included), PR #28136 body, file list and the three-dot diff origin/main...origin/claude/project-thread-f1kiqr (merge-base d6c0a75f, main at 277275aa), and the 59 check-runs on the head. Faces: reviewFace on the file list returns the schema face (hits = packages/spec/src/api/storage.zod.ts); the PR body and changeset also declare Clause-②: no (narrowing), so the narrowing floor adds every non-prose path. The changeset and content/docs/references/api/storage.mdx are the lane seat's ACCEPT at TIER_DEFAULT; ② below reads the changeset's declaration lines and level only.

① Derived judgments

  1. GetPresignedUrlRequestSchema.size accept set narrows from any JSON number to a non-negative integer (z.number().int().min(0)). RIGHT against the ruling (card: "int().min(0)") and against the door: isDeclaredByteCount on the head asks typeof size === 'number', Number.isInteger(size) and that size is at least 0 (storage-routes.ts:174-176), so -1, 1.5, -0.5, NaN, Infinity, '100' and true are refused by both. One residual edge is ④ S1.
  2. filename / mimeType narrow from z.string() to z.string().min(1). RIGHT: the door's presence check !filename || !mimeType || size == null (storage-routes.ts:970) has refused '' with 400 INVALID_REQUEST and written no row; the declared increment the claim conditioned on that measurement is met in the live tree, and the new door test pins it. Whitespace-only values stay admitted by both, as on the fix(spec)!: the chunked-upload init request declares filename and mimeType non-empty #27887 sibling.
  3. The public TS type GetPresignedUrlRequest = z.input stays string / string / number: no TS consumer moves. RIGHT: packages/client/src/index.ts:5369-5373 and :5436 compile unchanged, and the four Type Check jobs on the head are green.
  4. The door's accept set narrows in one place: a fractional size moves from 200 (URL minted, pending sys_file row written) to 400 INVALID_REQUEST, before store.createFile. RIGHT, and a narrowing in the same direction as the declaration, so the two refuse the same set for every realistic value. Status, code and ordering (presence, byte count, scope, upload limit) are unchanged for every other value; only the refusal text moves ("a non-negative whole number"), and no file outside storage-routes.ts / its test pins the old text (git grep on origin/main: none).
  5. Published JSON Schema: size becomes integer with minimum 0; filename / mimeType gain minLength 1. RIGHT; the regenerated reference table (storage.mdx:226 now integer, descriptions "(non-empty)") agrees. No other committed artefact records constraints: json-schema.manifest, api-surface, declaration-map, export-origins, authorable-surface, authorable-defaults list ids, keys and defaults only, so none is stale; the Lint gates that carry check:generated are green on the head.
  6. No key added, removed, renamed or tombstoned; UploadScope, scope default and bucket untouched; CompleteUploadRequestSchema and heldBytesRefusal untouched. RIGHT: the authorable-surface ledger needs no change and gets none.
  7. Public surface outside packages/spec: @objectstack/service-storage exports no new symbol; isDeclaredByteCount and PRESIGNED_SIZE_REFUSAL_MESSAGE stay module-private. RIGHT.

② Semver level

  • Changeset .changeset/27598-presigned-size-whole-non-negative.md: @objectstack/spec patch, @objectstack/service-storage patch; fix(spec)! with the BREAKING banner; Clause-②: no (narrowing) and Metadata-protocol: none at line starts; one ADR-0087 marker not-required (no-migration-prescription).
  • Clause-②: no (narrowing) is RIGHT. The diff widens nothing (①.6) and narrows a published accept set (①.1, ①.2, ①.4), so the arm is owed; the PR body carries the same line. The seat's correction 6524182832 keeps the claim's bare no as the truthful value and the arm as the breaking signal, which is how clause2-line.mjs reads it (the arm is the first token in the parentheses; the trailing " — ..." prose after it is permitted).
  • Level: none + patch is RIGHT under ruling A as written in check-changeset-no-major.mjs and the pr-automation level table: "Metadata-protocol: none — no change to what a metadata author can write: the level is patch, REST, drivers, services, ... fix!: alike". GetPresignedUrlRequestSchema is a REST request body no metadata type embeds; nothing an author writes changes. The script's own battery names this exact pair ("no (narrowing) with none beside it ships patch"). The fix(spec)!: the chunked-upload init request declares filename and mimeType non-empty #27887 sibling (InitiateChunkedUploadRequestSchema, merged 2026-10-30T03:34Z) carried the same pair. The head commit 4293804 is the regrade from minor + narrowed to this pair.
  • ADR-0087: not-required (no-migration-prescription) holds. The refused values never named a completable upload (a fraction cannot equal a held byte count; '' was already refused), so there is nothing for a migration to rewrite and no tombstone. The 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 precedent (semantic/18.upload-request-scope-closed.ts) registered an entry because callers had to choose a new vocabulary; here no caller choice exists. The gate step "Require an ADR-0087 disposition on a declared-breaking changeset" is green on every run of Check Changeset on this head, the failed one included.
  • Gate verdict: Check Changeset ran three times on this head: 2026-10-30T13:41Z failure at step 13 "Guard against accidental major bumps (launch window)", then 2026-10-30T13:50Z and 2026-10-30T13:53Z success after the body edit. The newest run of the name governs: green. (The failed job's log blob is not reachable from this seat; its step roster is, and the ADR-0087 step passed.)

③ Boundary flags

  • Author: PR user.login is objectstack-fleet[bot]; no login note owed.
  • open_questions[0] (keep Clause-②: no (narrowing) though the claim wrote bare no): ANSWERED by seat correction 6524182832, option A; the head carries the arm on body and changeset. Right (②).
  • open_questions[1] (narrowed + minor vs none + patch): ANSWERED by the same correction, option B (none); implemented in 4293804; Check Changeset green after the body edit. Right (②).
  • deviations[0] (first push refused, local branch renamed to the thread branch): mechanics only; Implemented-by below names the branch the diff lives on.
  • deviations[1] (arm differs from the claim): same as open_questions[0].
  • out_of_scope_findings[0] (SDK storage.upload sends mimeType: file.type, which is '' for a browser File of unknown type, so the presigned door answers 400): ESCALATED. Confirmed in the live tree (packages/client/src/index.ts:5371); pre-existing, not moved by this diff, and the changeset's remedy sentence ("an unknown type is sent as application/octet-stream") describes a behaviour the SDK helper does not have. Owes a card on packages/client (storage.upload falls back to application/octet-stream); not a finding on this PR.
  • out_of_scope_findings[1] (dogfood api-action-door-forward main-red candidate): RESOLVED on origin/main at 277275aa: the test now asserts expect(message).not.toContain('sys_metadata_activation') (:183). No card owed.
  • Check-runs on the head: 59; per name the newest run is success or skipped; the Governed Surface Queue Guard, Spec property liveness, PR-body guards, all Lint, Type Check, Build and the eight Test Core shards are green. Dogfood Regression Gate shards were skipped by the path filter on this PR; git grep on origin/main finds no dogfood or integration test that sends a fractional size or an empty filename / mimeType to /storage/upload/presigned, so the narrowing reaches no skipped suite.
  • Auto-merge: not armed. needs:contract-review is on the PR; a PASS on this head strips it.

④ Design judgment

packages/spec/src/api/storage.zod.ts (hits) — against the card's ruling (int().min(0)), the first consumer (POST /api/v1/storage/upload/presigned, storage-routes.ts:965-1046) and the SDK (packages/client/src/index.ts):

  • Holds. The declaration now names the set the door enforces for every JSON value a client produces; File.size is a whole number and the three descriptions read true. The comment cites storage: the chunked-upload init door refuses an empty filename / mimeType that InitiateChunkedUploadRequestSchema declares acceptable (z.string(), no min(1)) #27801 / the chunked-init sibling, whose filename / mimeType lines are byte-identical, so the two request schemas read as one convention.
  • S1, should-fix: @objectstack/spec pins zod ^4.6.6 (lockfile zod@4.6.7), where .int() is the safe-integer format: the schema refuses a size above Number.MAX_SAFE_INTEGER while the door's Number.isInteger admits it, and with no settings namespace bound (maxUploadBytes undefined, storage-limits.ts:228 admits any size) the door mints a URL for a value the declaration refuses — the same declared-versus-enforced class this PR closes for fractions, in the other direction. Reach is nil in practice (a declared size of 9 PB), so not a FAIL. Remedy: Number.isSafeInteger(size) in isDeclaredByteCount and one refusal-table row (Number.MAX_SAFE_INTEGER + 2), or a description that says the schema's integer is a safe integer.

packages/services/service-storage/src/storage-routes.ts (narrowing floor) — against heldBytesRefusal (:1976-1992) and the #22470 ordering rule:

packages/services/service-storage/src/storage-routes.test.ts (narrowing floor):

  • Holds. Each refusal row and both CONTROL rows now also ask the declaration the same question, so the test pins declaration and door to the same answer on every value it names; createFile is spied to pin "no row written". The empty-key test asserts status and code but not the presence-check message; acceptable, since the message is unchanged and pinned nowhere else either.

packages/spec/src/api/storage.test.ts (narrowing floor):

  • Holds. Refusals name the key and, for the strings, the too_small code; controls cover 0, 1, 1048576. The issues.toHaveLength(1) assertions bind the test to zod reporting one issue for -0.5 (int before min); green on this zod, a test-shape risk only, not a contract one.

Must-fix: none. Should-fix: S1.

Implemented-by: claude/project-thread-f1kiqr
Reviewed-by: session_01L9qibwb6hBqszPTYJobitm

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#1 · os-tesla · session_01L9qibwb6hBqszPTYJobitm

维护者速读

  • 改了什么: 预签名上传请求的 schema(GetPresignedUrlRequestSchema)现在声明 size 必须是非负整数,filename、mimeType 不能为空。上传入口同时开始拒收小数字节数,返回 400 INVALID_REQUEST。
  • 为什么改: 公开 schema 原来接受负数、小数和空字符串,入口却拒收负数和空值,按 schema 写代码的调用方或 AI 会被误导。小数更糟:入口原来会签发上传地址,并写一条永远无法完成的待上传记录。
  • 风险与回滚: 负数、非数字、空文件名或空类型的入口行为不变。唯一的行为变化是小数 size 从 200 变成 400,正常客户端发的 File.size 本来就是整数。回滚即 revert。
  • 先看: packages/spec/src/api/storage.zod.ts 的三处收窄;packages/services/service-storage/src/storage-routes.ts 的 isDeclaredByteCount。
  • 两分钟验证: pnpm --filter @objectstack/spec exec vitest run src/api/storage.test.ts,预期 73 个测试全过。
  • 需要你做的: 无。不属于受管面,席内契约复审 PASS(评论 6525353405),由席位落地。
  • 席位意见: ACCEPT,可落地。

席位核对 · ACCEPT · domain:spec seat 1 (#6017) · 2026-10-30T14:37Z

Head 4293804621b82769ea8a42241aeb0c5b833c20ca. Card #27598, claim 6521786446, claim correction 6524182832. The maintainer's rulings, verbatim: 「你有 fable,可以自己契约复审」 and 「按 pm-dispatch 审过的pr你可以自主落地」.

  • Shape: the PR is a draft to main, made by the relay. The body has Fixes #27598. The seat patched the body to Metadata-protocol: none, and its first line is now the seat signature, above the attribution block. Clause-②: no (narrowing) and Metadata-protocol: none sit line-initial on both the body and the changeset. Spec and service-storage are both patch, and the ADR-0087 disposition is not-required (no-migration-prescription).
  • Contract review: record 6525353405, in-seat at CONTRACT_REVIEW_TIER by an at-tier reviewer that was not the drafter. It is a PASS with no must-fix.
  • Accepted deviation: the claim wrote Clause-②: no. The tree, body and changeset carry no (narrowing), per the order and triage 6500594085. The correction 6524182832 set the protocol line to none.
  • Should-fix S1 dropped: zod 4 .int() refuses a size above Number.MAX_SAFE_INTEGER while the door's Number.isInteger admits it. The reach is a 9 PB declaration, so there is no user-visible cost and no card.
  • Gates: the newest run of every check on this head is success or skipped, and the Check Changeset re-run from this act's label change is the last one pending. mergeable_state is clean. check-governed-merges --pr 28136: NOT governed, +101/−17.
  • Body lag, noted rather than patched: the Verification block names 902d10c0 and says CI was not yet read. The storage suites, dogfood and gates were re-run on 42938046 by the dev (thread report). The tree is what the review judged.
  • Out of scope:
  • Landing: pr_ready + automerge_enable once the Check Changeset re-run is green.

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: GetPresignedUrlRequestSchema.size declares no lower bound; narrow it to int().min(0) (#27549 part b)

1 participant