Repository navigation
fix(rest)!: export refuses an unserved format and clone refuses a non-object overrides - #27659
Conversation
GET /data/:object/export mapped every format other than json and xlsx to csv, so format=pdf, format=xml and format= answered 200 with a csv file. An absent format is still csv; any other value outside csv, json, xlsx (the empty value included) now answers 400 VALIDATION_ERROR naming the value and the served set, before any row is read. Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
POST /data/:object/:id/clone passed overrides on only when typeof said object, so a string, number, boolean or null was dropped and the door answered 201 with a plain copy, and an array reached cloneData and was merged index-wise. A nested overrides or a bare body that is not a JSON object now answers 400 VALIDATION_ERROR naming overrides, before cloneData is called. An absent body or member is still a plain copy. Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
tsconfig.test.json reads Response.json() as unknown, so the body is cast to any where the test reads error.code; no assertion changes. Claude-Session: https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F Co-authored-by: Claude <noreply@anthropic.com>
…st-door-refuse-malformed
📓 Docs Drift CheckThis PR changes 1 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8067c249758c56ad01bb350a62fb3f0f93a103af && git checkout 8067c249758c56ad01bb350a62fb3f0f93a103af
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4d1f52c4458372169bf641f58db312734da5fb10 035ecdcf0a8a46b4f47bdb7cd4b3ca12a0abb735 && git checkout -B drift-repro 4d1f52c4458372169bf641f58db312734da5fb10 && git merge --no-ff 035ecdcf0a8a46b4f47bdb7cd4b3ca12a0abb735
node scripts/docs-audit/affected-docs.mjs --json 4d1f52c4458372169bf641f58db312734da5fb10
|
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F Contract-review-claim: session_01L6bhgUCizoYWQVJWjhdR6F · head 035ecdc · 2026-10-29T16:42Z |
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F Contract reviewServed-tier: Face: narrowing — the paths decide no face and the changeset's line-initial Inputs: cards #27500 and #27489 (bodies, triage, claims, the os-dev-report posted 2026-10-29T16:41Z on #27500, the pointer on #27489), PR #27659 body and file list, ① Derived judgmentsEvery accept-set and public-surface change the diff implies, judged against the live tree:
② Semver level
③ Boundary flagsDev flags (os-dev-report on #27500,
Two further boundary notes for the seat, neither a finding on this diff:
PR author is Check-runs on this head at this record's writing (2026-10-29T16:56Z): ④ Design judgmentPer in-scope file, against the code it cites and the scenario it is accepted against. Must-fix: none. Should-fix: none.
The narrowing against the published spec on Implemented-by: VERDICT: PASS Posted 2026-10-29T16:56Z. |
|
domain:server#2 · os-sales · session_01L6bhgUCizoYWQVJWjhdR6F 维护者速读(席位终稿)
席位意见: ACCEPT。两张卡同一文件合一个 PR;收窄已过契约审查,范围外的同类问题另开卡。 席位核对ACCEPT · head
|
Requested by Sales · project thread
Fixes #27500
Fixes #27489
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no skill teaches either door's malformed values, and
content/docs/api/data-api.mdxdescribes only the accepted shapes, which are unchanged维护者速读
GET /data/OBJ/export收到不支持的format(如 pdf、xml、空值)时返回 400,不再悄悄给一份 CSV;克隆接口POST /data/OBJ/ID/clone收到不是对象的overrides(字符串、数字、数组、null)时返回 400,不再悄悄当成普通复制。format、传 csv/json/xlsx、传对象 overrides 或不传 body 的行为完全不变。回滚即还原data-action-routes.ts的两处读取。packages/rest/src/data-action-routes.ts(导出的readExportFormat与克隆的isFieldMap守卫)、packages/rest/src/export-format-refusal.test.ts、packages/rest/src/clone-overrides-refusal.test.ts。pnpm --filter @objectstack/rest exec vitest run src/export-format-refusal.test.ts src/clone-overrides-refusal.test.ts,预期 2 个文件 28 条全部通过。What changes
One door, one reading per parameter, both in
packages/rest/src/data-action-routes.ts; one commit per card.format.readExportFormatreads the parameter: absent iscsv; a present value, trimmed and case-insensitive (the reading the route's owntemplate=truemode already givesformat), must be one of the route's served setcsv,json,xlsx. Anything else answers400through the shared nested writer (sendErrorfrom@objectstack/types):error.code: 'VALIDATION_ERROR', a message naming the value and the served set. The refusal comes beforefindData, so no row is read and no export header is set. The set is the route's own, not the spec's widerExportFormatenum (jsonl,parquetand the rest were never served and are refused);packages/specis not edited.format=(empty) is decided: refused. An empty value is a value the caller wrote, the same reading this route already giveslimit=(emptyIsAbsent: false) and thattemplate=truegivesformat=.overridesthat is not an object (a string) and answers 201 with the overrides silently ignored #27489, cloneoverrides. The handler still accepts both{ "overrides": {...} }and a bare field map. A nestedoverrides, or a bare body, that is not a JSON object (a string, number, boolean,nullor an array) answers400VALIDATION_ERRORnamingoverrides, beforecloneDatais called. That is the triage's locus: at the REST clone door, beforecloneData.packages/metadata-protocolis not edited."overrides": nullis decided: refused, not read as absent. A barenullbody stays what the transport already makes it (req.body ?? {}, a plain copy), and no body at all is a plain copy as before.check:route-envelopecounts no new hand-built body.VALIDATION_ERRORis aStandardErrorCodealready on the ledger; no code is minted.Tests
Both suites drive the real
HonoHttpServer(the adapteros servemounts), so the query string and the JSON body are parsed by the production code.packages/rest/src/export-format-refusal.test.ts: refusal forpdf,xml, empty, blank, andjsonl(in the spec enum, never served). Each assertsstatus 400,error.code, that the message names"format"and the set, that there is noX-Export-Format, and thatfindDatawas never called. Preservation: absent → csv with the row streamed;csv/CSV/json/JSON/json/xlsx/Xlsxeach answer their ownX-Export-Format.packages/rest/src/clone-overrides-refusal.test.ts: refusal for nested string, number,true,null, array, empty array, and bare array, string and number bodies. Each assertsstatus 400,error.code, the message naming"overrides", and thatcloneDatawas never called. Preservation: nested map, bare map,{"overrides":{}},{}and barenullreachcloneDatawith the expectedoverrides, and no body at all is a201plain copy.Acceptance notes
Clause-②: no (narrowing)where both claims sayClause-②: no. Both doors now refuse values they used to accept and answer as success, which is an accept-set narrowing (AGENTS.md Post-Task Checklist step 3). So the changeset carries the BREAKING banner, its migration, and the ADR-0087 disposition (not-required (no-migration-prescription)), atpatchlevel because the kind isnone. Seat: confirm or correct the arm.overridesand other top-level keys still reads onlyoverridesand ignores the rest, as before. This is outside both cards.test(rest): type the clone refusal test's parsed body(a cast thattsconfig.test.jsonneeded, folded separately because rewriting the pushed branch was refused in this session), then the changeset commit, then one merge oforigin/main.Verification
All readings are at head
035ecdcf(after one merge oforigin/main) unless a line says otherwise. Box seconds are shared-box seconds.Package tests and typecheck: @objectstack/rest green
pnpm --filter @objectstack/rest build && pnpm --filter @objectstack/rest test && pnpm --filter @objectstack/rest typecheckunder the verify lock:VERDICT command-exit 0. Test Files 337 passed (337), Tests 6615 passed and 326 skipped.tsc --noEmitwas clean, andcheck:test-typecheckwas OK with 0 files and 0 errors in the debt ledger.eslint --no-inline-config packages/rest: exit 0.Ablations: both negative pins go red with the old coercion put back, and are restored to blob == HEAD
Each leg used
node scripts/ablation-replace.mjsin wrap mode, with a shell trap restoringHEAD. The tests import./rest-serverfromsrc/, so nodist/leg applies.readExportFormat's return was replaced with the oldvalue === 'json' ? 'json' : value === 'xlsx' ? 'xlsx' : 'csv'. The anchor went from 1 to 0 and the blob from4a4b352aa086to39d51ee7fd0d. Result:export-format-refusal.test.tshad 5 failed and 8 passed (all five refusal cases:expected 200 to be 400). Restored: blob after restore4a4b352aa086== HEAD,git diff HEADempty.overridesthat is not an object (a string) and answers 201 with the overrides silently ignored #27489. The guard was removed and the old...(overrides && typeof overrides === 'object' ? { overrides } : {})pass-through put back. The anchor went from 1 to 0 and the blob from4a4b352aa086to8491a123aeeb. Result:clone-overrides-refusal.test.tshad 9 failed and 6 passed (all nine refusal cases:expected 201 to be 400). Restored: blob == HEAD,git diff HEADempty.Derived gate families: 59 derived, 59 run, all exit 0
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 59 commands at035ecdcf. Each was run with its exit code captured before any pipe, and all 59 exited 0.--ranreconciliation:59 derived famil(ies) accounted for — 59 run, 0 NOT-MEASURED (a DERIVED zero — all 59 recorded an exit code and none of them is 3).check:dual-build-cjs-loadshad nodist/yet, andcheck-plugin-teardown-shape --self-testwas on a shallow checkout. Both exited 0 on the final-head run above.merge-treeprobe was needed.Dogfood battery (required by the narrowing arm): 2 timeouts, both unrelated and green on re-run
pnpm --filter '@objectstack/dogfood^...' build && pnpm --filter @objectstack/dogfood testat035ecdcf:VERDICT command-exit 1, held 3349 s. Test Files: 2 failed, 349 passed, 3 skipped (354). Tests: 2 failed, 2820 passed, 20 skipped (2842).Test timed out in 5000ms:object-designer-field-reorder.dogfood.test.ts(creates and publishes the object in its declared order) andwall-package-refusal.dogfood.test.ts(a package install, upgrade and uninstall round trip). Neither file calls the data export or data clone door.--testTimeout=30000: Test Files 2 passed (2), Tests 24 passed (24). Read: box load, not this diff.Generated by Claude Code
🤖 Generated with Claude Code
https://claude.ai/code/session_01L6bhgUCizoYWQVJWjhdR6F