Skip to content

REST clone: a plain clone reports the source row's empty readonly columns as dropped fields (body and header) #29200

Closed

Description

@objectstack-fleet

domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

立卡门: ① 产品缺陷,具名落点 + 复现(reach: 公开入口 HTTP POST /api/v1/data/:object/:id/clone 实测一次错误结果,PR #29034 的开发在 main 135054eb 的真栈测试与 stock showcase 上各测一次,开发报告 6570533136)
业务理由: 用户在控制台克隆一条记录、什么都没改,控制台却提示「某些字段被丢弃」;集成方读 droppedFields 或 X-ObjectStack-Dropped-Fields 判断写入是否完整,会把每次克隆都当成丢了数据。不修,这个告警在克隆上失去意义。
动手的读者: domain:server 席,生产侧:packages/metadata-protocol/src/protocol.ts 的 cloneData(复制源行时带上空的只读列)或引擎插入时只读剥离对「键存在」的判断。⛔ 不在 applyDroppedFieldsHeader 过滤(复审裁定:头只镜像体,同一缝不立第二套策略)。
查重: clone droppedFields null readonly 4 命中、cloneData copies null readonly 4、clone phantom dropped field 4(均含已关);最近邻 #28933(头缺失,已关已修,本缺陷由其开发发现)、#14147(插入路径只读剥离只是协议边界守卫,已关)。

Follow-up of: #28933 (landing PR #29034; contract review on that PR, ③ "Dev finding (3)": separate card, fix site on the producer side)

What happens

A clone with no overrides of a row whose readonly column is empty (null) answers droppedFields naming that column, and since #29034 the X-ObjectStack-Dropped-Fields header repeats it. The caller never sent that field. The console announces every non-empty droppedFields, so the user sees a "fields dropped" notice on a plain clone.

Reproduction

  1. Boot the stock showcase.
  2. Pick a contact whose lead_score (readonly) is empty.
  3. POST /api/v1/data/contact/:id/clone with no body.
  4. Expected: 201, no droppedFields, no header.
  5. Actual: 201, droppedFields names lead_score, and the header names it too.

Where

cloneData copies the source row, null readonly columns included, into the insert; engine.insert's readonly strip reports any present key whatever its value. Fix one of the two so that a key the caller did not supply and the clone carried as empty is not reported. Which one is the dev's reading; keep the strip's report for a readonly value a caller really sends.

Acceptance


Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobs
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 2, 2026
  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: p2 · domain:server · area:api · pm:queue · 2026-11-02T09:57Z

    ⛔ Not a claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

    Claim: PM loop round 1 · card thread · 2026-11-02T10:09Z
    Session: session_01ADHkmQxwoGb5fSnrFkpxmt (the seat); the card thread is session_01ADHkmQZyRQCzui7zekMYY7
    Account: os-steve (the seat's linked user; the card's assignee)
    Branch: claude/project-thread-v06ygi (the card thread's harness-pinned branch; it names no issue, so this claim is the identity record)
    Worktree: the card thread's own container checkout
    Domain: domain:server
    Seat: domain:server#2
    Ruling-ref: the maintainer, verbatim: 「开 server 2、3、5 号席位, 前任已经下班,接手所有开发,包括跟进之前未完成的开发落地。」 Card: none from the maintainer on this card; filed by this seat as a landing follow-up, graded p2 by triage.
    File surface: packages/metadata-protocol/src/protocol.ts (cloneData only, under the delta's route (a)); tests; the one dogfood file showcase-static-readonly.dogfood.test.ts; changeset. ⛔ No logic change in packages/rest/src/error-response.ts, no ui/**, no packages/spec/**.
    Container & model: M, mode:thread, model: opus (TIER_DEFAULT; --tier names no path mandate: PM's call), effort: medium
    Clause-②: no (expected: a body and header lose a false entry; no request refused)
    Metadata-protocol: none (expected)
    Responsibility: the code is cloneData in @objectstack/metadata-protocol · the platform path is a plain clone announcing "fields dropped" for empty readonly columns the caller never sent · reached by every console clone and every integration that reads droppedFields or X-ObjectStack-Dropped-Fields
    Thread-read: none
    Premise: holds (measured by the dev of the parent PR; the card thread re-measures red first on current origin/main)
    Serial constraints cleared: open PR #28731 edits protocol.ts at probeRecord, not cloneData (hunks read this act); the card thread keeps its hunks off it and merges origin/main before pr_create
    Resume-from: none — fresh from origin/main

    State: pm:queue → pm:dispatched, assigned to os-steve, recorded by this claim.


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQZyRQCzui7zekMYY7
    os-dev-report

    {
     "issue": 29200,
     "status": "done",
     "branch": "claude/project-thread-v06ygi",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/29290",
     "session": "session_01ADHkmQZyRQCzui7zekMYY7",
     "premise_still_valid": true,
     "summary": "Producer-side fix under route (a), narrowed to EMPTY values: cloneData (packages/metadata-protocol/src/protocol.ts) now leaves a readonly field whose copied source value is null/undefined out of the insert payload, so engine.insert's strip no longer reports a key nobody supplied; body droppedFields and the X-ObjectStack-Dropped-Fields header both lose the false entry. Stored row unchanged (the strip removed the key from a non-system caller anyway; applyFieldDefaults treats explicit null as omitted). Acceptance 3 measured: a readonly column that HOLDS a value is reported on main (body + header ro;reason=readonly) and still is after the fix; judged NOT the same defect because the #15703 ruling (option 1, record 5578819560) requires the clone to report a copied-and-stripped value, so (a) is not applied to valued columns. Overrides setting a readonly field are still reported (control). No change to packages/rest/src/error-response.ts, ui/** or packages/spec/**.",
     "tests": "Red first at main 45e9b2f1 (tests only, rest^... built): rest-dropped-fields-every-write-door.test.ts Tests 1 failed | 17 passed (18), the new plain-clone-of-empty case (expected ... to not have property droppedFields); valued and overrides controls green on main. Fix f825d6bd: 18/18. Ablation via scripts/ablation-replace.mjs (anchor 1->0, blob 3ff88bdc6abc->15c970b77f49, line made dead by name === '__ABLATION_29200__'), ablation-dist-preflight marker in dist/index.js + index.cjs: rest pin 1 failed | 17 passed; dogfood showcase-static-readonly 1 failed | 8 passed (body named lead_score). Restore legs: blob == HEAD, git diff HEAD empty, rebuilt, preflight --absent exit 0, 18/18 and 9/9. metadata-protocol test+typecheck: 300 passed/4 skipped files, 22100 tests passed, VERDICT command-exit 0. rest test+typecheck: 367/367 files, 6966 passed/332 skipped, VERDICT command-exit 0. After merging origin/main (85ebba60): install, rest^... + dogfood^... closure build, spec check:generated VERDICT command-exit 0; rest pin 18/18, dogfood file 9/9.",
     "gates": "At 85ebba60: dispatch-gates --commands derived 65; all 65 recorded exit 0 (check:dual-build-cjs-loads first exit 3 PREREQUISITE NOT MET, re-run exit 0 after turbo build ./packages/**). dispatch-gates --ran: 65 run, 0 NOT-MEASURED, 0 UNRUN (DERIVED zero). Ran list: /mnt/project-files/server-seat2/29200-gates-ran.txt. Lint: no package lint script; root eslint --no-inline-config on the 3 touched files exit 0. CI check-runs on #29290: not awaited (in_progress at report time).",
     "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm; the one touched dogfood file was run: Tests 9 passed (9) at 85ebba60",
     "line_budget": "+78 / -6 over 4 files (protocol.ts +12/-0, rest test +38/-3, dogfood +13/-3, changeset +15/-0)",
     "files_changed": [
      "packages/metadata-protocol/src/protocol.ts (cloneData only)",
      "packages/rest/src/rest-dropped-fields-every-write-door.test.ts",
      "packages/qa/dogfood/test/showcase-static-readonly.dogfood.test.ts",
      ".changeset/29200-clone-empty-readonly-not-dropped.md"
     ],
     "deviations": [
      "Route (a) applied to EMPTY readonly values only, not 'do not carry a readonly column at all' as the delta words it: dropping valued readonly columns would silence the #15703-ruled report (acceptance 3 decision, stated in the PR body).",
      "Forced test edits: dogfood clone-with-overrides header toContain -> toBe('lead_score;reason=readonly') plus comment; rest test comment above CLONE_OBJECT rewritten and one seed row src_empty added (no assertion moved). Both named in the PR body."
     ],
     "mcp_calls": "3 — issue_read x3 (29200 get, 29200 get_comments, 28933 get_comments); no GitHub write tool. Other reads were gh api REST GETs (29200 comments, 29034 review comment 6570720694, #15703 and its comments, PR 29034 body).",
     "api_writes": "2 — fleet-write pr_create (#29290, run 41434211047, 9352 bytes sent = stored); this os-dev-report comment on #29200 via post-stamped. git push is not a REST write.",
     "open_questions": [],
     "out_of_scope_findings": []
    }

    Stamped 2026-11-02T11:09Z.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions