Skip to content

fix(rest): every 2xx write answer whose body reports droppedFields also sends the X-ObjectStack-Dropped-Fields header - #29034

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-gibefk
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-gibefk

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #28933

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:克隆、批量创建(createMany)、按对象 batch、updateMany、导入提交和跨对象 /batch 这六个写入口,只要响应正文里带 droppedFields,现在也会发 X-ObjectStack-Dropped-Fields 响应头,和单条创建、更新一样,走同一个 applyDroppedFieldsHeader。
  • 为什么改:只看响应头的 API 集成方,在批量创建和克隆上收不到"只读字段被剥掉"的提示,会以为字段写进去了;同一个契约在不同写入口说法不一。
  • 风险与回滚:只加响应头,正文和状态码一个字节不变,没剥字段时不发头。批量入口的头只能写并集(说不清哪一行剥了什么),逐行明细仍在正文。回滚即 revert 本 PR。
  • 先看:packages/rest/src/error-response.ts(头的格式去重、读逐行报告);packages/rest/src/batch-routes.ts(createMany、batch、updateMany、跨对象 batch 四处调用);packages/rest/src/data-action-routes.ts(克隆与导入提交;导入预演不发头)。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/rest-dropped-fields-every-write-door.test.ts,预期 15 个用例全过。
  • 需要你做的:无,席位落地。

What changes

Single POST /data/:object and PATCH /data/:object/:id advertised a legal write strip (readonly, a true readonlyWhen) in the body and in the X-ObjectStack-Dropped-Fields header (#3431). Every other write door that reports the same strip in its body now sends the header too, through the existing applyDroppedFieldsHeader and in the same field;reason=REASON format. There is one seam and no second formatter.

door where the body reports it header
POST /data/:object/:id/clone top-level droppedFields the clone's own strips
POST /data/:object/createMany top-level droppedFields, already the rows' union that union
POST /data/:object/batch results[i].droppedFields the union of the rows' strips
POST /data/:object/updateMany results[i].droppedFields the union of the rows' strips
POST /data/:object/import, commit results[i].droppedFields the union of the rows' strips
POST /batch, cross-object top-level droppedFields, one event per op with its index the union
  • The seam. applyDroppedFieldsHeader(res, result, rows?) reads result.droppedFields and, when a door passes them, each row's droppedFields. The formatter now emits each field;reason token once. A single-record answer never repeats a token, so its header value is unchanged (the existing rest: PATCH /data 对被静默剥离的写入字段无任何回传 — onFieldsDropped 通道未接线(follow-up #3407/#3413) #3431 cases pass unedited).
  • The union. The header cannot say which row, op or object dropped a field. On a bulk door it names the union, each token once, and the body keeps the per-row report. A field one row strips and another does not is named once.
  • The stale roll-up answer. Clone's and createMany's 201 over a write that landed under a failing roll-up (data writes: form submit, batch / *ManyData, cloneData, the MCP create_record bridge and callData's fallback still answer 500 after a landed write when the summary recompute fails #28782) now goes through earnedWriteDespiteStaleSummary, which sets the header as single create's earned answer already did.
  • Import dry run. It writes nothing, so it is not a write answer and sends no header. Its rows still preview the strip in the body.
  • Words that went false. The spec .describe() text on clone's and createMany's droppedFields and on the batch row (BatchOperationResultSchema) said these answers carry no header. So did the CloneDataResult docblock in @objectstack/client and the clone section of content/docs/api/data-api.mdx. They now say the header is sent; content/docs/references/api/{batch,protocol}.mdx are regenerated (gen:schema && gen:docs). No key, type or status changes.
  • Changeset. @objectstack/rest, @objectstack/spec and @objectstack/client patch. It states that a bulk header is the union and the body keeps the per-row detail.

Tests

  • packages/rest/src/rest-dropped-fields-every-write-door.test.ts (new, 15 cases): RestServer over the real ObjectStackProtocolImplementation and ObjectQL on better-sqlite3. It covers every door in the table: header value, body unchanged, a mixed createMany naming the field once, rows stripping different fields, and a no-strip control per door. It also covers the import dry run sending no header, and the stale roll-up answer of clone and createMany through a protocol double.
  • packages/qa/dogfood/test/showcase-static-readonly.dogfood.test.ts (four cases added) on the stock showcase as admin: clone with overrides.lead_score and a two-row createMany carrying lead_score send lead_score;reason=readonly; a mixed createMany names it once; a createMany that strips nothing sends no header.

Acceptance notes

  • The order's hypotheses, measured. Red first: confirmed. On main the new rest pin went 11 red with the 4 controls green, and the dogfood clone case answered no header. Controls: confirmed, and the single create and update headers are unchanged. Ablation: confirmed, each call reds only its own door's cases (table below).
  • The card's createMany path. The card wrote the bare-array createMany as POST /data/:object. That route refuses an array body (400 VALIDATION_FAILED, 请求体从不与声明它的 schema 对照(#3877 的请求侧对偶):7 个 schema 定义了从未启用,而 API 目录已宣称生效 #3899). The bare-array door is POST /data/:object/createMany, which is the door fixed here.
  • A clone reports the source's empty readonly columns. cloneData copies every column it does not reset, null ones included, and the engine's strip reports a present key whatever its value. So a plain clone of a row whose readonly column is empty answers droppedFields naming that column (measured on main: a clone of a row with null ro2, and a clone of a showcase contact with no override, both name the column). The header now repeats what the body says. This PR does not change the producer. That is why the clone cases use objects of their own and the dogfood clone case asserts with toContain. It is handed to the seat as a finding.
  • Reversal of a recorded stance. rest/protocol: 把 droppedFields 写路径观测扩展到 bulk / GraphQL / client SDK(#3431 D2 波及面 follow-up) #3455's changeset said the header is "deliberately not emitted for batches" because one header cannot express per-row drops. The order's ruling settles that: the header is the union and the body keeps the per-row detail. The batch-row .describe() text is updated to say so.
  • Not changed. Public form submit: its answer carries no droppedFields. The async import job: its 202 carries none, and its results route is a read. The runtime dispatcher's POST/PATCH /data arms (the @objectstack/hono catch-all twin) also set no header, for single create and update too. That is outside this card's REST doors; observed, not measured live.
  • Overlap. Before opening, I re-read the open PRs touching crud-routes.ts, batch-routes.ts, data-action-routes.ts, error-response.ts and the two spec files. fix(metadata-protocol): the by-id OCC comparison answers only for a row the caller reads #28731 is the only nearby one, and its files are in metadata-protocol, runtime and dogfood. No shared file.

Verification

Everything below was measured at a6b0d839.

Red first on main (135054e + the new tests only)
rest-dropped-fields-every-write-door.test.ts   Tests  11 failed | 4 passed (15)
  every header assertion: expected undefined to be 'ro;reason=readonly' (…)
showcase-static-readonly.dogfood.test.ts       clone with overrides.lead_score: expected null to be 'lead_score;reason=readonly'
Green
pnpm --filter @objectstack/rest exec vitest run rest-dropped-fields-every-write-door rest-dropped-fields rest-batch-endpoint import-row-dropped-fields-20701 data-write-summary-recompute-other-doors rest-stored-expression-dropped-fields rest-update-path-id-not-a-dropped-field
  Test Files  7 passed (7) · Tests  66 passed (66)
pnpm --filter @objectstack/dogfood exec vitest run test/showcase-static-readonly.dogfood.test.ts
  Tests  8 passed (8)
pnpm --filter @objectstack/rest typecheck && pnpm --filter @objectstack/client typecheck   VERDICT command-exit 0
pnpm --filter @objectstack/rest test   Test Files  367 passed (367) · Tests  6951 passed | 332 skipped (7283) · VERDICT command-exit 0
Ablation (scripts/ablation-replace.mjs, anchor hits 1, restore proven blob == HEAD each time)
call removed red cases (of 15)
clone success clone overrides case (1)
clone stale answer clone earned case (1)
createMany success the three createMany strip cases (3)
createMany stale answer createMany earned case (1)
per-object batch batch case (1)
updateMany updateMany case (1)
import commit import commit case (1)
import dry-run guard (header sent on a dry run) import case (1)
cross-object batch cross-object case (1)
seam: each token once updateMany, batch, import, cross-object (4)
seam: per-row reading updateMany, batch, import (3)

Dist-resolved dogfood leg: the clone and createMany calls were replaced by a marker, rest was rebuilt, and ablation-dist-preflight found the marker in dist. Three dogfood cases went red (clone, two-row createMany, mixed createMany), with 5 passed. After the restore, rest was rebuilt, the marker was absent from dist and the tree was clean, and all 8 passed.

Derived gates (dispatch-gates --commands, local subset)
106 derived, 106 run, every one :: exit 0 at a6b0d839 (dispatch-gates --ran: 0 NOT-MEASURED, 0 UNRUN).
First pass: 8 gates answered exit 3 (PREREQUISITE NOT MET: spec dist predated the describe edit; the teardown self-test needed its anchor commit fetched) and check:generated exit 1 on the same stale dist.
After pnpm --filter @objectstack/spec build and the fetch, all 9 reran exit 0.
The full list is in the os-dev-report comment.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FEkQ5nE2GswUTcEX8HbTmt


Generated by Claude Code

…ropped-fields header

Clone, createMany, the per-object batch, updateMany, an import commit and
the cross-object batch reported a legal write strip in the body only. They
now call applyDroppedFieldsHeader as single create and update do. The seam
reads a door's per-row reports when passed and names each token once, so a
bulk header is the union of the rows' strips; the body keeps the per-row
detail. An import dry run writes nothing and sends no header.

The spec describe text, the client CloneDataResult docblock and the data
API docs stop saying these answers carry no header; the reference docs are
regenerated.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5nE2GswUTcEX8HbTmt
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/client, @objectstack/rest, @objectstack/spec, touching 9 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/rest/src/rest-server.ts, packages/rest/src/stale-summary-answer.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/data-api.mdx (via CloneDataResponseSchema (symbol, a top-level const))

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

  • content/docs/releases/v17/17-4.mdx (via CloneDataResponseSchema (symbol, a top-level const))

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
  • 2 changed file(s) yielded no anchor (packages/rest/src/rest-server.ts, packages/rest/src/stale-summary-answer.ts) — pages documenting those are invisible to this run
  • 1 cross-cutting symbol(s) contributed no route anchor: applyDroppedFieldsHeader (5 routes)
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 — 175 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 ab085a048241b0bf1fde06696fc6cbf659a6707f → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 ab085a048241b0bf1fde06696fc6cbf659a6707f → 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#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a6b0d839d45a4e08037beab8bd4efb60b2fcb369
Local-runs: none
Scope: packages/spec/src/api/batch.zod.ts, packages/spec/src/api/protocol.zod.ts + packages/rest/src/error-response.ts, packages/rest/src/batch-routes.ts, packages/rest/src/data-action-routes.ts, packages/rest/src/stale-summary-answer.ts, packages/rest/src/rest-server.ts, packages/rest/src/crud-routes.ts (read, untouched), packages/rest/src/form-routes.ts (read, untouched), packages/client/src/index.ts, content/docs/api/data-api.mdx, content/docs/references/api/batch.mdx, content/docs/references/api/protocol.mdx, .changeset/28933-dropped-fields-header-every-write-door.md, packages/rest/src/rest-dropped-fields-every-write-door.test.ts, packages/qa/dogfood/test/showcase-static-readonly.dogfood.test.ts

Face: schema (the recogniser's hits are the two packages/spec/src/** files; both are .describe() text only — no key, type, optionality or status changes). PR #29034, branch claude/project-thread-gibefk, author objectstack-fleet[bot], one commit over merge-base 135054eb. Reviewed read-only: the net diff against main at the head, card #28933 with every comment, #3431, #3455 (its landing PR #3468's changeset), #15703 (its landing PR #16823), and the head's check-runs. The dispatch order was not an input to the judgments below.

① Derived judgments

  1. The published spec surface (the hits). BatchOperationResultSchema.droppedFields (batch.zod.ts), CloneDataResponseSchema.droppedFields and CreateManyDataResponseSchema.droppedFields (protocol.zod.ts) change .describe() prose only: "REST does not emit X-ObjectStack-Dropped-Fields for batches" / "sets no X-ObjectStack-Dropped-Fields header" become "REST also sends the header, naming the union (each field and reason once)" / "REST additionally surfaces this as the header". No accept set moves: every request schema is untouched and every response member keeps its key, type and optionality. Right — and required, because the old text became false the moment the routes changed; a .describe() that promises the absence of a header the route now sends would be a spec stating a falsehood. git grep on the head tree finds no remaining "body only / no header" carrier outside CHANGELOGs.

  2. One seam, byte-identical single answers. applyDroppedFieldsHeader(res, result, rows?) (error-response.ts) now reads result.droppedFields plus each rows[i].droppedFields, and droppedFieldsHeaderValue builds the value through a Set of field;reason=REASON tokens joined by ', ' in insertion order. The old formatter was flatMap + join(', ') over the same tokens. For a single-record answer the two are byte-identical unless the producer emitted the same (field, reason) pair twice; createData / updateData / cloneData (protocol.ts) push one engine onFieldsDropped event per strip point, so no duplicate exists on a single write, and the pre-existing rest: PATCH /data 对被静默剥离的写入字段无任何回传 — onFieldsDropped 通道未接线(follow-up #3407/#3413) #3431 suite is unedited. The union format is rest: PATCH /data 对被静默剥离的写入字段无任何回传 — onFieldsDropped 通道未接线(follow-up #3407/#3413) #3431's grammar, unchanged: same token, same separator. Neither function is exported from @objectstack/rest's index, so the signature change is internal. Right.

  3. Coverage of every 2xx write answer carrying droppedFields. The REST write doors at the head and their answers: POST /data/:object and PATCH /data/:object/:id (header, pre-existing); POST /data/:object/:id/clone (now the seam, success and stale-roll-up 201); POST /data/:object/createMany (seam, success and stale-roll-up 201); POST /data/:object/batch and /updateMany (seam over result.results); POST /data/:object/import commit (seam over summary.results); cross-object POST /batch (seam over the top-level droppedFields). Doors whose answers carry no droppedFields, read in the cited code: /deleteMany, /import/jobs (201 { jobId, object, status, total, createdAt }), /import/jobs/:jobId/cancel ({ success }), /import/jobs/:jobId/undo ({ success, jobId, deleted, restored, failed, failures }), public /forms/:slug/submit ({ id, fields? } — its droppedFields is dropped by the rest(public forms): POST /forms/:slug/submit answers the anonymous submitter with the whole stored record, so any field a hook derives from existing data (a duplicate match, an owner) reaches the internet #22437 projection, so earnedAnswerLogged without the header is correct there), and single delete's stale-roll-up answer (no drops on a delete). The PATCH / DELETE single doors' stale answers already went through earnedWriteDespiteStaleSummary. No 2xx write answer that carries droppedFields is left without the header. The runtime dispatcher's POST/PATCH /data arms (packages/runtime) are a different package and door family, not this card's; the dev's "noted, not filed" is the right disposition, and the seat may file it when reach is measured.

  4. Bodies unchanged byte for byte. Every call is placed between the protocol result and res.json(...) of the same object; the only body-adjacent edit is the cross-object batch's literal becoming const answer = { results, ...droppedFields? } with the same keys in the same order. The stale-roll-up branches return the same earned object as before. The seam writes only a header and returns before touching res when the value is empty, so no header is sent when nothing was stripped; the new suite has a no-strip control for clone, createMany, updateMany + batch, import commit and cross-object batch, and the dogfood pin one for createMany. Right.

  5. The reversal of rest/protocol: 把 droppedFields 写路径观测扩展到 bulk / GraphQL / client SDK(#3431 D2 波及面 follow-up) #3455. PR feat(rest/protocol): extend droppedFields write-observability to bulk paths + client SDK (#3455) #3468's changeset and the three describe texts said the header is "deliberately not emitted for batches — one response header cannot express per-row drops, so the per-row body field is the canonical bulk channel". That stance was a design rationale, not an acceptance-set guarantee a client could consume: the body remains the canonical per-row channel and is unchanged, the header is additive on the wire, and a client that inferred "nothing stripped" from the header's absence on a bulk door was already wrong by that same text. The new describe text keeps the honest half ("a single response header cannot say which row dropped what") and states the union. Right, and the changeset says so in words.

  6. File surface wider than the claim. The claim named crud-routes.ts; the card's "around L304 and L495" are the single create / update calls, which were correct at main, but the clone and createMany handlers live in data-action-routes.ts and batch-routes.ts after [maintainer] refactor(rest): split packages/rest/src/rest-server.ts (13,701 lines, 70 commits in 30 days) along its register*Endpoints seams, one move-only slice per PR, /meta after #22877 stage 2 #23050's split. The nine non-test, non-prose files touched are: the seam (error-response.ts), the six doors (batch-routes.ts, data-action-routes.ts), a comment on the twin helper (stale-summary-answer.ts), a comment on the re-export (rest-server.ts), the three describe texts and the client docblock (words that went false), data-api.mdx (same), and the two regenerated reference pages (gen:docs output; check:generated would red without them). Each is either the ruling's "measure the other write doors and give them the same call" or a carrier that would otherwise state the opposite of what the tree does. Justified; nothing unrelated rides along.

  7. Evidence, as claimed and as checkable read-only. The ablation table (11 legs) is consistent with the committed 15-case file: "each token once" reds exactly the four doors whose events repeat a field (updateMany, batch, import, cross-object; createMany's body is already a union), "per-row reading" reds the three results[i] doors, each door leg reds its own cases. The 106-line dispatch-gates --ran list in the seat's copy carries 106 exit 0. One count does not reconcile: the committed file has five no-strip control cases by my reading (clone, createMany, updateMany + batch, import commit, cross-object), so red-first on main reads as 10 red / 5 green rather than the reported 11 / 4; the difference is one control case, most likely a later edit to the file after the red-first run, and the head's gates, not the red-first count, are the verdict. Overlap: all 45 open PRs' file lists were read through the REST API; none touches any of this PR's non-test files or the dogfood file (the dev's fix(metadata-protocol): the by-id OCC comparison answers only for a row the caller reads #28731 reading holds).

② Semver level

③ Boundary flags

  • Open question — import dry run sends no header (shipped as option A). Recommendation: keep A. The header's rest: PATCH /data 对被静默剥离的写入字段无任何回传 — onFieldsDropped 通道未接线(follow-up #3407/#3413) #3431 meaning is "fields stripped from THIS write"; a dryRun: true answer is a 200 preview that wrote nothing, so a header there would say a write dropped fields when no write happened, and the rows already preview the strip in results[i].droppedFields. The guard is one line and the import case pins both arms, so if the seat rules B it is a one-line flip with the pin's expectation inverted; the changeset already states the dry-run behaviour, so no further carrier needs editing for A.
  • Dev finding (3) — a plain clone reports the source's null readonly columns as dropped, and the header now repeats it. This is a producer defect on main (cloneData copies null readonly columns; the engine's strip reports a present key whatever its value), measured by the dev on main at 135054eb and on the stock showcase. The header is, by the ruling, a mirror of what the body reports; having the seam filter what the body says would create a second policy on the one seam. Separate card, class a, fix site on the producer side (clone's copy or the strip's "present key" reading), with the dev's dedupe words; this PR neither causes nor widens the defect beyond mirroring the body it already carried, and the test file's clone cases and the dogfood toContain assertion are the right accommodation. The seat should file it before or at landing so the mirror has a tracked producer fix.
  • Dev finding (4) — the card's createMany path. The card and triage's note wrote the bare-array createMany as POST /data/:object; that route refuses an array body (400 VALIDATION_FAILED, 请求体从不与声明它的 schema 对照(#3877 的请求侧对偶):7 个 schema 定义了从未启用,而 API 目录已宣称生效 #3899). The real bare-array door is POST /data/:object/createMany, which is what the PR fixes and the dogfood pin hits. Premise correction, not a premise failure: the defect (body droppedFields, no header) was real on the actual door, so premise_still_valid: true stands in substance; the record notes the path so the card's repro step 3 is read as /createMany.
  • The runtime dispatcher twin (packages/runtime/src/domains/data.ts, no header even for single create/update): outside this card's REST doors; reach unmeasured. Not a flag against this PR; the seat may file it once reach is measured.
  • CI state on the head at review time: 38 check-runs completed (26 success, 12 skipped), 0 failed, 29 still in progress (Test Core 1–8, Test UI 1–16, Build Core, Type Check workspace and consumer gates, Type Check UI, Build and E2E UI). Governed Surface Queue Guard, Check Changeset, PR-body guards, Lint ESLint, Lint PM dispatch-gates, Spec property liveness and Dogfood Regression Gate are green. The landing check "all checks green" is the seat's at enqueue; this review's verdict does not depend on it and nothing red exists to be read as by-design.
  • Author is objectstack-fleet[bot]; no human login to name.

Implemented-by: claude/project-thread-gibefk
Reviewed-by: session_0186vfjHHMsRXC9EUT9hTFMo

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-11-02T06:34Z · head a6b0d839 · claimed by seat 2 on #28933 (comment 6569737073)

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape:
  2. Contract review at CONTRACT_REVIEW_TIER: PASS on this head (6570720694).
  3. Seat rulings:
  4. Gates: CI on this head showed 35 success, 12 skipped, 0 red and 20 still running at the time of this ACCEPT. Auto-merge waits for them, and the merge queue re-runs CI against current main.
  5. Follow-up: the dev's finding that a plain clone reports the source's null readonly columns as dropped will be filed as its own card at landing.
  6. Landing: pr_ready + automerge_enable through the relay.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 06:35
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 06:35
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 078e581 Nov 2, 2026
74 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-gibefk branch November 2, 2026 07:40
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

Landing record · PR #29034

domain:server seat 2 · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-11-02T07:45Z · card #28933

Reading Probe Result
Merged PR #29034 state merged, squash commit 078e58130
On main git merge-base --is-ancestor 078e58130 origin/main yes
Queue refs git ls-remote origin refs/heads/gh-readonly-queue/* matching 29034 0
Content git grep -c applyDroppedFieldsHeader origin/main -- packages/rest/src/batch-routes.ts present

Release: ships patch for rest, spec and client at the next release.


Generated by Claude Code

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/l tests tooling

Projects

None yet

1 participant