You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
REST deleteMany: ids [""] is refused with the sentence "requires 'ids' to be an array of record ids" and an empty fields[], although ids is an array — the entry at fault is not named #29137
立卡门类别: ① 产品缺陷 — 具名落点 (packages/metadata-protocol/src/protocol-batch-data.ts deleteManyData, the scalar-id check ~line 937) + 复现.
reach: 公开入口实测一次错误结果 — POST /api/v1/data/showcase_private_note/deleteMany {"ids":[""]} answers 400 whose sentence says ids must be an array and whose fields is empty (stock showcase boot at 7fa9873).
业务理由: a client whose id list carries one blank entry (an unset selection, a trimmed CSV column) is told its payload is not an array, which it is, and is not told which entry is wrong. The refusal itself is correct; its sentence sends the developer to check the wrong thing. The same door names the entry for [null] (fields[0] {field: ids.0, code: invalid_type}) and updateMany answers a blank id with a per-row "Record id is required", so the platform's own error-sentence contract already says what this answer should look like. Not doing it: one of the bulk doors keeps a misleading refusal.
动手的读者: the domain:server seat (metadata-protocol); triage first touch confirms the lane, then os-dev.
查重: deleteMany empty id → 81 hits incl. closed; nearest #24781 (closed: /batch empty-string id answered 500, a different door and shape); "requires 'ids' to be an array" → 0 hits. Not among the wave-4 cards.
QA-source: #29124 · api-backend.bulk-write-contract · observed beside the clauses
Reproduction (stock showcase, admin):
POST /api/v1/data/showcase_private_note/deleteMany {"ids":[""]} → 400 VALIDATION_FAILED, message "deleteMany on 'showcase_private_note' requires 'ids' to be an array of record ids", fields: [].
Control: {"ids":[null]} → 400 with fields[0] {field: "ids.0", code: "invalid_type"}.
Control: {"ids":[" "]} reaches the per-row loop and answers RECORD_NOT_FOUND for that row.
Control: POST …/updateMany {"records":[{"id":"","data":{}}]} → per-row VALIDATION_FAILED "Record id is required".
Expected: a 400 that names the offending entry (ids.0, empty id), as for [null].
Actual: a sentence about the array's type and an empty fields[].
Read fresh. The fix lands in deleteMany's scalar-id check in packages/metadata-protocol/src/protocol-batch-data.ts (the "requires 'ids' to be an array" refusal, near line 900), so domain:server. It is p2 as a user-visible behaviour defect: the 400 names the wrong problem and returns an empty fields[] where the [null] case already names ids.0, so the error contract shape is wrong, not just the copy. It is a concrete defect with a named location and a repro, so pm:queue; it was observed beside the clauses, not as a FAIL of the P1 item, so the RC p1 rule does not apply.
Claim: PM loop round 1 · 2026-11-02T08:32Z
Session: session_01ADHkmQjPiXgNoSaEpqeGvY (seat); dev = card thread session_01ADHkmQJyGvWVm7dbChPMZ7
Account: os-steve
Branch: claude/project-thread-t9p12i (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
Worktree: the card thread's own
Domain: domain:server
Seat: domain:server#1
File surface: deleteManyData in packages/metadata-protocol/src/protocol-batch-data.ts (the scalar-id check), its tests and a @objectstack/metadata-protocol changeset. ⛔ No packages/spec/**, no new error code, no new gate; the fail-closed rule for non-scalar entries is unchanged. (stop on breach; explain in the report)
Container & model: S, mode:thread, model as the thread was opened, effort: medium
Clause-②: no (expected: the same requests stay refused; the sentence and fields[] name the offending entry)
Metadata-protocol: none (expected)
Responsibility: the code is the bulk delete door of @objectstack/metadata-protocol · the platform path is a deleteMany whose blank id is refused with a sentence about the array's type and an empty fields[] · reached by every REST client of deleteMany, today
Thread-read: 6572487185
Premise: holds (seat read on maind659c1d; the dev re-measures on base first)
Serial constraints cleared: No open PR touches protocol-batch-data.ts. This seat's #29093 and #29066 edit other metadata-protocol files.
Orders: the seat's standard order plus the card delta, in the project's shared folder (server-seat1/standard-order.md, server-seat1/29137-delta.md).
{
"issue": 29137,
"status": "done",
"branch": "claude/project-thread-t9p12i",
"pr": "https://github.com/objectstack-ai/objectstack/pull/29198",
"session": "session_01ADHkmQJyGvWVm7dbChPMZ7",
"premise_still_valid": true,
"summary": "deleteManyData now splits its scalar-id check: a non-array ids keeps the old sentence and no fields; an array with bad entries is still refused 400 VALIDATION_FAILED, with one fields[] row per offending entry ({field: 'ids.N', code, message}, the shape the REST ingress gives [null]). Empty string answers required, any other non-id answers invalid_type; the sentence names the first offender. What is refused is unchanged ([\"\"] still reaches the row loop as RECORD_NOT_FOUND). Base fea2dbb7 re-measure: [\"\"] and [\"a\",\"\"] got the array sentence with fields [] at the REST door; [null], [{$ne:null}] and a non-array are answered by the ingress zod parse (ids.0 / ids invalid_type); in-process all five got the array sentence and no fields.",
"tests": "head 9e074bf5. pnpm --filter @objectstack/metadata-protocol test: Test Files 297 passed | 4 skipped, Tests 22069 passed | 26 skipped; typecheck exit 0. REST: the 17 packages/rest/src test files naming deleteMany: Test Files 17 passed, Tests 1049 passed. New cases: protocol.delete-many.test.ts ([''], ['a',''], [{$ne:null}], [null], several at once, non-array, [' ']) and rest-delete-many-ingress.test.ts (real protocol at the REST door: ['a',''] -> ids.1 required; [null] -> ids.0 invalid_type). Ablation via ablation-replace.mjs --hold + metadata-protocol rebuild, two legs: (1) drop err.fields: 5 protocol cases red ('expected undefined to deeply equal [ { field: ids.0 ... } ]') + REST case red ('expected [] to deeply equal [ [ ids.1, required ] ]'); (2) accept empty string as id: 3 protocol cases red + REST case red ('expected 200 to be 400'). Each leg restored (blob == HEAD, git diff HEAD empty); final rebuild + ablation-dist-preflight ABLATION_29137 --absent passed. The REST suite reads metadata-protocol dist, so its red proves each mutation reached dist; the preflight present-lines for the two legs were filtered out of my capture.",
"gates": "dispatch-gates --commands (no paths) derived 60 at head 9e074bf5: 58 exit 0; node scripts/check-plugin-teardown-shape.mjs --self-test exit 3 on shallow clone (fixture commit 621a4876 missing), exit 0 after git fetch --depth=1 of that commit; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (needs whole-workspace dist) = NOT MEASURED, CI Lint & Repo Gates runs it. --ran reconciliation: 60 derived, 60 run, 0 UNRUN. pnpm lint family: neither package declares a lint script; pnpm exec eslint --no-inline-config on the 3 touched files exit 0. CI check-runs: in_progress at report time.",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
"mcp_calls": "3 — mcp__github__issue_read get_comments x2 (claim check), mcp__github__pull_request_read get x1 (draft + body read-back); no write tools",
"api_writes": "2 — fleet-write relay pr_create (PR #29198, read-back 5460/5460 bytes identical, no assignees); post-stamped os-dev-report comment on #29137",
"open_questions": [
{"question": "Which field code should an empty-string id carry?", "options": ["A required — the same fault the batch door names 'Record id is required for delete' (rowRequiredIdError)", "B min_length — what a z.string().min(1) ingress would spell through zodIssuesToFields"], "recommendation": "A, because an empty id is a missing id, and the sibling batch door already calls it required; both are existing catalog members, so either is one-line."}
],
"out_of_scope_findings": [],
"files_changed": [".changeset/29137-delete-many-names-refused-id.md", "packages/metadata-protocol/src/protocol-batch-data.ts", "packages/metadata-protocol/src/protocol.delete-many.test.ts", "packages/rest/src/rest-delete-many-ingress.test.ts"],
"line_budget": "+145 / -1 (4 files; source +36/-1)",
"deviations": [
"File surface: added a REST door test in packages/rest/src/rest-delete-many-ingress.test.ts (the delta names 'its tests'; this pins the wire answer the card reports). No source outside protocol-batch-data.ts.",
"check:dual-build-cjs-loads NOT MEASURED locally (prerequisite: full workspace build); left to CI.",
"Main moved from the seat's d659c1d9 to base fea2dbb7; no conflict, no merge needed."
]
}
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: api-backend.bulk-write-contract
Triage:
domain:server· p2 ·pm:queue· 2026-11-02T08:23Z⛔ Not a claim.
Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY
Claim: PM loop round 1 · 2026-11-02T08:32Z
Session:
session_01ADHkmQjPiXgNoSaEpqeGvY(seat); dev = card threadsession_01ADHkmQJyGvWVm7dbChPMZ7Account:
os-steveBranch:
claude/project-thread-t9p12i(the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)Worktree: the card thread's own
Domain:
domain:serverSeat:
domain:server#1File surface:
deleteManyDatainpackages/metadata-protocol/src/protocol-batch-data.ts(the scalar-id check), its tests and a@objectstack/metadata-protocolchangeset. ⛔ Nopackages/spec/**, no new error code, no new gate; the fail-closed rule for non-scalar entries is unchanged. (stop on breach; explain in the report)Container & model:
S,mode:thread, model as the thread was opened,effort: mediumClause-②: no (expected: the same requests stay refused; the sentence and
fields[]name the offending entry)Metadata-protocol: none (expected)
Responsibility: the code is the bulk delete door of
@objectstack/metadata-protocol· the platform path is a deleteMany whose blank id is refused with a sentence about the array's type and an emptyfields[]· reached by every REST client ofdeleteMany, todayThread-read: 6572487185
Premise: holds (seat read on
maind659c1d; the dev re-measures on base first)Serial constraints cleared: No open PR touches
protocol-batch-data.ts. This seat's #29093 and #29066 edit other metadata-protocol files.Orders: the seat's standard order plus the card delta, in the project's shared folder (server-seat1/standard-order.md, server-seat1/29137-delta.md).
objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-steve · session_01ADHkmQJyGvWVm7dbChPMZ7
os-dev-report
{ "issue": 29137, "status": "done", "branch": "claude/project-thread-t9p12i", "pr": "https://github.com/objectstack-ai/objectstack/pull/29198", "session": "session_01ADHkmQJyGvWVm7dbChPMZ7", "premise_still_valid": true, "summary": "deleteManyData now splits its scalar-id check: a non-array ids keeps the old sentence and no fields; an array with bad entries is still refused 400 VALIDATION_FAILED, with one fields[] row per offending entry ({field: 'ids.N', code, message}, the shape the REST ingress gives [null]). Empty string answers required, any other non-id answers invalid_type; the sentence names the first offender. What is refused is unchanged ([\" \"] still reaches the row loop as RECORD_NOT_FOUND). Base fea2dbb7 re-measure: [\"\"] and [\"a\",\"\"] got the array sentence with fields [] at the REST door; [null], [{$ne:null}] and a non-array are answered by the ingress zod parse (ids.0 / ids invalid_type); in-process all five got the array sentence and no fields.", "tests": "head 9e074bf5. pnpm --filter @objectstack/metadata-protocol test: Test Files 297 passed | 4 skipped, Tests 22069 passed | 26 skipped; typecheck exit 0. REST: the 17 packages/rest/src test files naming deleteMany: Test Files 17 passed, Tests 1049 passed. New cases: protocol.delete-many.test.ts ([''], ['a',''], [{$ne:null}], [null], several at once, non-array, [' ']) and rest-delete-many-ingress.test.ts (real protocol at the REST door: ['a',''] -> ids.1 required; [null] -> ids.0 invalid_type). Ablation via ablation-replace.mjs --hold + metadata-protocol rebuild, two legs: (1) drop err.fields: 5 protocol cases red ('expected undefined to deeply equal [ { field: ids.0 ... } ]') + REST case red ('expected [] to deeply equal [ [ ids.1, required ] ]'); (2) accept empty string as id: 3 protocol cases red + REST case red ('expected 200 to be 400'). Each leg restored (blob == HEAD, git diff HEAD empty); final rebuild + ablation-dist-preflight ABLATION_29137 --absent passed. The REST suite reads metadata-protocol dist, so its red proves each mutation reached dist; the preflight present-lines for the two legs were filtered out of my capture.", "gates": "dispatch-gates --commands (no paths) derived 60 at head 9e074bf5: 58 exit 0; node scripts/check-plugin-teardown-shape.mjs --self-test exit 3 on shallow clone (fixture commit 621a4876 missing), exit 0 after git fetch --depth=1 of that commit; pnpm check:dual-build-cjs-loads exit 3 PREREQUISITE NOT MET (needs whole-workspace dist) = NOT MEASURED, CI Lint & Repo Gates runs it. --ran reconciliation: 60 derived, 60 run, 0 UNRUN. pnpm lint family: neither package declares a lint script; pnpm exec eslint --no-inline-config on the 3 touched files exit 0. CI check-runs: in_progress at report time.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm", "mcp_calls": "3 — mcp__github__issue_read get_comments x2 (claim check), mcp__github__pull_request_read get x1 (draft + body read-back); no write tools", "api_writes": "2 — fleet-write relay pr_create (PR #29198, read-back 5460/5460 bytes identical, no assignees); post-stamped os-dev-report comment on #29137", "open_questions": [ {"question": "Which field code should an empty-string id carry?", "options": ["A required — the same fault the batch door names 'Record id is required for delete' (rowRequiredIdError)", "B min_length — what a z.string().min(1) ingress would spell through zodIssuesToFields"], "recommendation": "A, because an empty id is a missing id, and the sibling batch door already calls it required; both are existing catalog members, so either is one-line."} ], "out_of_scope_findings": [], "files_changed": [".changeset/29137-delete-many-names-refused-id.md", "packages/metadata-protocol/src/protocol-batch-data.ts", "packages/metadata-protocol/src/protocol.delete-many.test.ts", "packages/rest/src/rest-delete-many-ingress.test.ts"], "line_budget": "+145 / -1 (4 files; source +36/-1)", "deviations": [ "File surface: added a REST door test in packages/rest/src/rest-delete-many-ingress.test.ts (the delta names 'its tests'; this pins the wire answer the card reports). No source outside protocol-batch-data.ts.", "check:dual-build-cjs-loads NOT MEASURED locally (prerequisite: full workspace build); left to CI.", "Main moved from the seat's d659c1d9 to base fea2dbb7; no conflict, no merge needed." ] }Stamped 2026-11-02T09:12Z.
objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY
ACCEPT → PR #29198 comment 6573381274 (head
9e074bf5c9). Readied and auto-merge armed through the relay; lands when CI is green.objectstack-fleet commentedon Nov 2, 2026
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY
Landed · 2026-11-02T10:41Z
761fef4013, through the merge queuecompletedbyFixes #29137pm:dispatchedis removed. Seat record: claim 6572652287, seat ACCEPT 6573381274 (Clause-②: no, no contract review owed).