Skip to content

fix(rest)!: clone refuses a body that carries overrides beside other top-level keys - #27934

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-1eeldu
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-1eeldu

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Fixes #27669

Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no skill teaches the clone door's body shapes, and content/docs/api/data-api.mdx describes only the two accepted shapes, which are unchanged

维护者速读

  • 改了什么:克隆接口 POST /data/OBJ/ID/clone 的请求体若同时带 overrides 和其它顶层键(例如 {"overrides":{"name":"Copy"},"status":"draft"}),现在返回 400,不再只读 overrides、悄悄丢掉 status 并返回 201。
  • 为什么改:调用方以为两个值都生效了,实际只生效一半;与 data: POST /api/v1/data/:object/:id/clone accepts an overrides that is not an object (a string) and answers 201 with the overrides silently ignored #27489 修过的「静默丢弃」同一类。
  • 风险与回滚:风险是混用两种请求体形状、曾「侥幸成功」的调用方现在收到 400;单独的 {"overrides":{...}}、不带 overrides 键的裸字段表、{}、无 body 行为不变。回滚即删掉克隆处理器里的这一段守卫。
  • 先看:packages/rest/src/data-action-routes.ts(克隆处理器的 strays 守卫)、packages/rest/src/clone-overrides-refusal.test.ts(新的 [#27669] 一组)。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/clone-overrides-refusal.test.ts,预期 19 条全部通过。
  • 需要你做的:无,席位复核与合同复审后落地。

What changes

Tests

packages/rest/src/clone-overrides-refusal.test.ts (the #27659 harness, real HonoHttpServer) gains a [#27669] block:

Verification

Pin on main, then the fix, then an ablation
  • Pin first: the [#27669] block was committed alone on top of main b13a1ed3 (commit b276331d). vitest run src/clone-overrides-refusal.test.ts: 3 failed, 16 passed, each expected 201 to be 400.
  • With the fix (e2d1eb18): 19 passed (19).
  • Ablation through node scripts/ablation-replace.mjs, anchor if (strays.length > 0) { replaced by if (strays.length < 0) {: anchor 1 → 0, blob 327a925b243e → 71a31169d8a4; result 3 failed, 16 passed. Restored: blob after restore 327a925b243e == HEAD, git diff HEAD empty. The test imports ./rest-server from src/, so no dist/ leg applies.
Package tests and typecheck: @objectstack/rest green at the final head
  • At 9d859e3a (after one merge of origin/main, install and closure rebuild): pnpm --filter @objectstack/rest test exit 0, Test Files 348 passed (348), Tests 6754 passed and 332 skipped.
  • pnpm --filter @objectstack/rest typecheck at e2d1eb18: exit 0, check:test-typecheck OK with 0 files / 0 errors in the debt ledger.
Derived gate families: 59 derived, 58 exit 0, 1 NOT MEASURED; pnpm lint exit 0
  • node scripts/pm/dispatch-gates.mjs --commands derived 59 commands at e2d1eb18, each run with its exit code captured before any pipe. 58 exited 0 (check-plugin-teardown-shape --self-test after fetching its pinned commit into the shallow clone).
  • pnpm check:dual-build-cjs-loads exited 3, PREREQUISITE NOT MET (it needs every package's dist/): NOT MEASURED locally; CI's Build Core job runs it.
  • --ran reconciliation: 59 derived famil(ies) accounted for.
  • pnpm lint: exit 0.
  • The merge of origin/main changed none of this PR's three files' other regions; data-action-routes.ts auto-merged.
Dogfood battery (owed by the narrowing arm): green at the final head

Reviewer notes


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4czA6HpNWK4YvhLTw3x

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

os-dev-report · #27669 · session_016WV4czA6HpNWK4YvhLTw3x

{
  "card": 27669,
  "pr": 27934,
  "head": "9d859e3a",
  "branch": "claude/project-thread-1eeldu",
  "session": "session_016WV4czA6HpNWK4YvhLTw3x",
  "outcome": "done",
  "premise": "holds — pinned red on main b13a1ed3: 3 cases answered 201 where 400 was expected",
  "files_changed": [
    "packages/rest/src/data-action-routes.ts",
    "packages/rest/src/clone-overrides-refusal.test.ts",
    ".changeset/27669-clone-overrides-stray-keys.md"
  ],
  "line_budget": "+68 / -1 vs merge base, 3 files; human-merge threshold 3000: under",
  "tests": "clone-overrides-refusal.test.ts 19/19 at e2d1eb18; ablation (strays.length > 0 -> < 0) 3 failed/16 passed, restored blob == HEAD; @objectstack/rest test at 9d859e3a: 348/348 files, 6754 passed, 332 skipped; typecheck exit 0",
  "gates": "59 derived at e2d1eb18: 58 exit 0, pnpm check:dual-build-cjs-loads exit 3 (NOT MEASURED, needs whole-tree dist); --ran reconciliation 59 accounted; pnpm lint exit 0",
  "dogfood_battery": "9d859e3a · exit 0 (no os-verify-lock; single worktree) · Test Files 357/357 (3 skipped) · Tests 2875 passed, 20 skipped · no failures. Earlier run at e2d1eb18 before the merge: 4 failures, 3 = stale hook-abort pins of #27806 (fixed on main by #27878), 1 = platform-app-object-entry-views sys_exchange_rate; all pass at 9d859e3a",
  "deviations": [
    "Skills-docs: none — content/docs/api/data-api.mdx describes only the two accepted shapes, which are unchanged (same reading as #27659)",
    "dual-build-cjs-loads not measured locally"
  ],
  "declarations": "Clause-②: no (narrowing); Metadata-protocol: none; BREAKING banner + adr-0087 not-required marker in the changeset; Fixes #27669",
  "mcp_calls": "5 — reads only: issue_read x3 (get, get_comments, get), pull_request_read x1, search_issues x1; no write tool",
  "api_writes": "3 via relay — pr_create, pr_request_reviewers, comment",
  "open_questions": [],
  "out_of_scope_findings": []
}

Generated by Claude Code

@github-actions github-actions Bot added 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 1 package(s): @objectstack/rest, touching 2 documentable anchor(s).

17 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json ca54eacaa13befaea8142274a4db20b10fce2ab8.

⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

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 — 19 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 ca54eacaa13befaea8142274a4db20b10fce2ab8 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 ca54eacaa13befaea8142274a4db20b10fce2ab8 → 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#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract-review-claim: session_016WV4czuTWNgKR22fKv7jia · head 9d859e3afe40b913d629631afa3a1ec39dab6dbd · 2026-10-30T04:32Z

The lane seat runs the CONTRACT_REVIEW_TIER review this PR owes (Clause-②: no (narrowing)).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9d859e3afe40b913d629631afa3a1ec39dab6dbd
Local-runs: none
Scope: packages/rest/src/data-action-routes.ts, packages/rest/src/clone-overrides-refusal.test.ts, .changeset/27669-clone-overrides-stray-keys.md + packages/client/src/index.ts (data.clone), content/docs/api/data-api.mdx (POST /data/:object/:id/clone), .changeset/27500-27489-rest-door-refuse-malformed.md (#27659 precedent)

① Derived judgments

  • Guard narrows exactly the ruled shape. nested = isFieldMap(body) && 'overrides' in body; only when nested and Object.keys(body) holds a key other than overrides does the door refuse. Nested shape alone (strays = []), a bare field map with no overrides key (nested = false), {}, bare null (req.body ?? {} → {}) and no body all fall through untouched; the 15 pre-existing data: POST /api/v1/data/:object/:id/clone accepts an overrides that is not an object (a string) and answers 201 with the overrides silently ignored #27489 cases pin them and stay green (19/19 reported).
  • Before any write. The refusal sits after auth/api-access and the cloneData capability check, before cloneData.call — no protocol or engine call precedes it. Order versus data: POST /api/v1/data/:object/:id/clone accepts an overrides that is not an object (a string) and answers 201 with the overrides silently ignored #27489: the stray check runs first, so {"overrides":"x","status":"draft"} is refused for the stray key rather than the non-object overrides; either way 400 VALIDATION_ERROR before cloneData, and the data: POST /api/v1/data/:object/:id/clone accepts an overrides that is not an object (a string) and answers 201 with the overrides silently ignored #27489 message still applies once the body is a pure nested shape. Sensible: the shape fault is the outer one.
  • Envelope consistent with fix(rest)!: export refuses an unserved format and clone refuses a non-object overrides #27659. Same writer (sendError as sendEnvelopeError from @objectstack/types), same status and code (400, VALIDATION_ERROR), nested ADR-0112 error.code/error.message. The message echoes only the caller's own stray key names (quoted), never values, never record data — the same discipline as describeNonFieldMap, which names a kind, not a value.
  • Callers. SDK packages/client data.clone sends { overrides } or {} only — unaffected. No data.clone( or /data/.../clone caller in ui/, objectui, packages/cli, dogfood or examples (the only UI clone doors are permission-set/automation/sharing-rule clones on other routes). content/docs/api/data-api.mdx documents only the two accepted shapes; the mixed shape was never published. Nobody in-repo is refused.
  • overrides-named field. The handler passes body.overrides to cloneData as the override map, so { "overrides": { "overrides": "x", "name": "Copy" } } reaches cloneData as { overrides: 'x', name: 'Copy' } — pinned by the new preservation case (201). The remedy in the refusal message and the changeset is therefore correct and the only shape that sets such a field; matches the triage ruling (no new shape).
  • Tests. Real HonoHttpServer harness (production body parser); pinned red on main b13a1ed3 (3 fail), green with fix, ablation > 0 → < 0 re-reds exactly the 3 cases, blob restored == HEAD. Asserts status, code, every stray key, both shapes, the nested-overrides prescription and cloneData never called.

② Semver level

@objectstack/rest patch with ! title and BREAKING banner — identical disposition to #27659 (accept-set narrowing at a REST data door, no key or export changes). Clause-②: no (narrowing) on PR body and changeset; Metadata-protocol: none correct (no protocol surface touched). Skills-docs: none, same reading as #27659 (docs describe only the accepted shapes, which are unchanged). Level and markers correct.

③ Boundary flags

  • ADR-0087 marker not-required (no-migration-prescription) with reasoning: correct — no schema key added/removed/renamed, no type or export change, nothing for a migration to rewrite.
  • No governed path, no published schema, no spec change; diff confined to the clone handler, its harness and one changeset (dispatch's file surface held; the serial-note import regions untouched, origin/main auto-merged).
  • Non-blocking nit (not a must-fix): content/docs/api/data-api.mdx says "a bare field map is also accepted" without saying the two shapes are exclusive; a half-sentence there would make the refusal discoverable before it is hit. Optional follow-up, consistent with fix(rest)!: export refuses an unserved format and clone refuses a non-object overrides #27659's Skills-docs reading.

④ Design judgment

Narrowing judged against the published spec on main: the docs and the SDK declare exactly two body shapes, nested or bare, and no consumer was ever promised a mix. The change takes no declared capability away; it makes the door's own contract real where it previously answered 201 while applying half the request — the same silent-drop class #27489 closed one shape over. Refusing the mix (rather than merging the stray keys into the overrides) is the right call: merging would invent a third shape and make overrides ambiguous as a field name, whereas the refusal plus the nested prescription keeps the field-named-overrides case expressible with no new surface. Dogfood battery at the final head 9d859e3a: exit 0, 357/357 files, 2875 passed, 20 skipped, no failures (the 4 earlier reds at e2d1eb18 were tracked main-side pins, gone after the merge). Head check-runs were still in progress at review time; the landing is conditioned on green CI as the dispatch says.

Implemented-by: claude/project-thread-1eeldu
Reviewed-by: session_016WV4czuTWNgKR22fKv7jia (seat subagent)

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Seat ACCEPT · PR #27934 · head 9d859e3afe40b913d629631afa3a1ec39dab6dbd · 2026-10-30T04:49Z

维护者速读

Sometimes a body sent to POST /api/v1/data/:object/:id/clone carries overrides and also another top-level key. That body is now refused with 400 VALIDATION_ERROR before anything is cloned. The refusal names each stray key and both accepted shapes. Before, the door read only overrides, silently dropped the rest, and answered 201. These shapes are unchanged:

  • the nested shape alone;
  • a bare field map with no overrides key;
  • {}, null, or no body.

A record whose real field is named overrides nests it ({ "overrides": { "overrides": … } }); that case is pinned.

席位核对

  • Shape: draft → main, Fixes #27669.
    • Three files: the clone handler guard in data-action-routes.ts (+17 / −1), its harness test, and a changeset.
    • check-governed-merges --pr 27934: NOT governed, +68 / −1.
  • Contract review: CONTRACT_REVIEW_TIER record 6515072706, PASS at this head. Claim 6515036761.
    • Non-blocking nit, left as is: data-api.mdx could state that the two shapes are exclusive.
  • Declarations: Clause-②: no (narrowing), Metadata-protocol: none, patch, BREAKING banner, ADR-0087 not-required (no-migration-prescription). Consistent, and the same disposition as fix(rest)!: export refuses an unserved format and clone refuses a non-object overrides #27659.
  • Callers: the SDK data.clone sends only { overrides } or {}. No caller in the repo sends the mixed shape.
  • Dogfood battery: full and green at this head, with 2875 passed (os-dev-report 6515007501).
  • Gate check-runs on this head: 48 total, 35 success, 13 skipped by the path filter, 0 failed. mergeable_state: clean.
  • Landing: Tier S, under the maintainer's landing grant for this term (2026-10-29 22:05Z, seat thread). Ready + auto-merge through the relay.

ACCEPT

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 30, 2026 04:50
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 30, 2026 04:50
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 30, 2026
Merged via the queue into main with commit dd9921e Oct 30, 2026
49 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-1eeldu branch October 30, 2026 05:37
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

Development

Successfully merging this pull request may close these issues.

rest: POST /data/OBJECT/ID/clone with overrides plus other top-level keys silently drops the other keys

2 participants