feat(spec,metadata-protocol): cloneData reports droppedFields like every other create face — CloneDataResponseSchema gains the optional member - #16823
Conversation
… schema member, listener, pins, changeset Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
…neDataResponse.droppedFields Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
…onedata-dropped-fields
📓 Docs Drift CheckThis PR changes 3 package(s): 3 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 133 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 3e9d711477f9432bf4015de6685b069b50e1c4af && git checkout 3e9d711477f9432bf4015de6685b069b50e1c4af
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d4401f75bbe2e9e1b6f52f9bad83e377c5c33277 d117857520a6d6037dd8e1b4627b145b4a0eb390 && git checkout -B drift-repro d4401f75bbe2e9e1b6f52f9bad83e377c5c33277 && git merge --no-ff d117857520a6d6037dd8e1b4627b145b4a0eb390
node scripts/docs-audit/affected-docs.mjs --json d4401f75bbe2e9e1b6f52f9bad83e377c5c33277
|
…s member Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
Contract review (
|
| Part | Evidence | Verdict |
|---|---|---|
1 · optional droppedFields on CloneDataResponseSchema, same Zod element as CreateDataResponseSchema |
packages/spec/src/api/protocol.zod.ts:2021 — droppedFields: z.array(DroppedFieldsEventSchema).optional().describe(…); CreateDataResponseSchema at :1975 is z.array(DroppedFieldsEventSchema).optional(). Same element schema (packages/spec/src/data/data-engine.zod.ts:271), same optionality. Only the .describe() prose differs (header sentence — see §3). |
done |
2 · cloneData passes onFieldsDropped like createData |
packages/metadata-protocol/src/protocol.ts:10902-10905 vs createData at :10808-10811. Byte-identical pattern: const dropped: DroppedFieldsEvent[] = []; const opts: any = { onFieldsDropped: (e) => { dropped.push(e); } }; if (ctx !== undefined) opts.context = ctx; … engine.insert(object, data, opts); return spreads ...(dropped.length > 0 ? { droppedFields: dropped } : {}) at :10917, same as createData:10826. DroppedFieldsEvent was already imported (:83). The strip itself is untouched (engine-side). |
done |
| 3 · two conformance pins + #15395 firing-control pin flip in the same PR | search-clone-schema-conformance.test.ts:249-280 (pin 2 → "declared AND produced") and :282-295 (omit-when-empty, retains not.toHaveProperty('droppedFields') on the no-drop body). protocol.readonly-insert.test.ts:266-267 (was toBeUndefined(), now typeof … toBe('function')), enumeration grows 6→7 (:260-261). PR #15395 is merged (b398ad258 in origin/main), so the flip fights nothing. |
done |
4 · @objectstack/spec minor |
.changeset/clone-data-dropped-fields.md:2 — '@objectstack/spec': minor. |
done |
Beyond the ruled three files — judged owed, not widening:
packages/client/src/index.ts:432-451—CloneDataResultgainsdroppedFields?: DroppedFieldsEvent[]. The pre-PR docblock at the same site said, in a shipped.d.ts, "NodroppedFields: unlikecreateData, the clone producer emits no write-observability event" — a sentence part 2 makes false. Also, without it a TS caller ofclient.data.clonereads the member only via cast, i.e. the declared mirror would drift from the schema it names as its source (Spec: CloneDataResponseSchema). Owed under "a published claim your change falsifies is yours". Additive type widening ⇒@objectstack/clientminor is the correct level (WHICH LEVEL: "a new accepted key or value takes at least minor"). Keep it.packages/objectql/src/protocol-data.test.ts:657-660— the pin assertedinsertwas called with exactly{ context: ctx }; part 2 falsifies it. Owed (the card did not name it; a change that turns an existing exact-args pin red must carry the flip). Test-only inobjectql, so noobjectqlchangeset entry is owed (nothing released).content/docs/api/data-api.mdx:358-364— the clone section stated the 201 body as exactly{ object, id, sourceId, record }; falsified, corrected, and the correction states body-only (no header). Owed, non-governed.
2. File enumeration vs merge-base — governed paths: NONE
.changeset/clone-data-dropped-fields.md (+15)
content/docs/api/data-api.mdx (hand-written, non-governed)
content/docs/references/api/protocol.mdx (GENERATED — see below)
packages/client/src/index.ts
packages/metadata-protocol/src/protocol.readonly-insert.test.ts
packages/metadata-protocol/src/protocol.ts
packages/metadata-protocol/src/search-clone-schema-conformance.test.ts
packages/objectql/src/protocol-data.test.ts
packages/spec/authorable-surface/api.json (+1 line)
packages/spec/src/api/protocol.zod.ts
git diff --name-only <mb>..head -- 'docs/adr/**' '.claude/**' 'skills/**' AGENTS.md CLAUDE.md 'content/docs/releases/**' returns empty. No governed path is touched ⇒ a maintainer-only merge is NOT required on governed-surface grounds. (The Clause-② tier gate is a separate floor; this comment is that review.)
content/docs/references/api/protocol.mdx is generated output: its header carries {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. … */} with Source: packages/spec/src/api/protocol.zod.ts; the writer is packages/spec/package.json:254 gen:docs: tsx scripts/build-docs.ts (and check:docs is its --check). The +11 lines are the new row and the Nested Shape: CloneDataResponse.droppedFields[number] table, i.e. exactly what the new .describe() projects. packages/spec/authorable-surface/api.json diff is numstat 1 0 — the single added key api/CloneDataResponse:droppedFields (line 313); authorable-surface.base.json untouched.
3. Clause-② derivation from code
A published response contract (CloneDataResponseSchema, @objectstack/spec/api, declared AS PRODUCED under the 2026-08-25 ruling on #11924) gains an optional member. Additive: a reader of object/id/sourceId/record sees no change; omit-when-empty means a no-drop clone's body is byte-identical to before. Clause-②: yes is correctly declared in the PR body and the needs:contract-review label is present on both PR and card (check-clause2-carriers pair, per the dev report).
The .describe() at protocol.zod.ts:2021-2030 states: "Carried in the 201 body only — this route relays the producer verbatim and sets no X-ObjectStack-Dropped-Fields header." Measured true: packages/rest/src/rest-server.ts:8865-8872 — the clone handler does const result = await cloneData.call(p, {…}); res.status(201).json(result); with no applyDroppedFieldsHeader between them. That helper (packages/rest/src/error-response.ts:2472) is called at exactly two sites in rest-server.ts: :8617 (single create) and :8759 (single update). So the schema prose, data-api.mdx, the client docblock and the changeset all state the same measured fact. Header asymmetry judged in F1.
4. Changeset grading
.changeset/clone-data-dropped-fields.md: @objectstack/spec: minor, @objectstack/metadata-protocol: minor, @objectstack/client: minor. No major (launch-window guard, check-changeset-no-major.mjs, green by construction). No BREAKING banner, so check-adr-0087-registration.mjs is not engaged (its own G1: "a non-breaking changeset is not this gate's business") — correct, since an optional additive member is not breaking and no ADR-0087 disposition is owed.
LEVEL axis (#16055, same script :734-1025): PUBLISHED_SOURCE_PATH = /^packages\/([^/]+)\/src\// (:822). Packages this diff grows: spec, metadata-protocol, client, and objectql (via protocol-data.test.ts). Predicate is entry.bump === 'patch' && grown.has(entry.pkg) (:945): none of the three entries is patch, and objectql has no entry (absence is not flagged; test-only, nothing released) ⇒ clean. Against batch #35 WHICH LEVEL (pr-automation.yml:667-682): "a purely additive widening of a published package's public surface … takes at least minor" — all three widened packages (schema member, 201 body member, .d.ts type member) are graded minor. Correct on every axis. Its own report of #16713 (packages/spec/src, packages/metadata-protocol/src, packages/client/src all match the regex) is consistent with what I derive.
5. Tests
Pins that redden if the listener wiring in cloneData is removed (by reading; each asserts on something only the listener produces):
packages/metadata-protocol/src/protocol.readonly-insert.test.ts·cloneData forwards the copied row AND the caller overrides whole — and reports the engine's verdict on both(:148-155,res.droppedFields toEqual [{ fields: ['approval_status','source'] }]).- same file ·
every create face whose response carries droppedFields passes an onFieldsDropped listener to the engine(:260-264, 7th insert'soptions.onFieldsDroppedwould beundefined). - same file ·
cloneData passes the listener too — the sixth face …(:281). packages/metadata-protocol/src/search-clone-schema-conformance.test.ts·emits no top-level key the spec does not declare — droppedFields included, now that it is declared AND produced (#15703)(:268-270).packages/objectql/src/protocol-data.test.ts·cloneData › forwards context to findOne and insert(:657-660, exact{ context, onFieldsDropped: expect.any(Function) }).
The PR's ablation table reports exactly these five going red (4 + 1); consistent with the reading.
Conformance fixture is no longer vacuous: search-clone-schema-conformance.test.ts:192-224 — makeCloneProtocol now declares approval_status: { readonly: true, defaultValue: 'draft' } on customer, and the engine double iterates the object's fields, deletes each readonly key present in the payload, and calls options.onFieldsDropped({ object, fields, reason: 'readonly' }) exactly when ≥1 was dropped (:212-222). The produced-branch case seeds the SOURCE row with approval_status: 'approved' (:258) — the clone-specific path where the caller typed nothing — and asserts the event, the stripped record, and safeParse success both raw and over-the-wire (:268-280). The readonly-insert fixture (:60-93) already stripped-and-fired before this PR; its clone case now asserts on it.
Omit-when-empty: :282-295 — default source row carries no readonly key ⇒ double fires nothing ⇒ expect(body).not.toHaveProperty('droppedFields') still holds, and now against a double that CAN drop, so it is a real omit-when-empty pin rather than an artefact of a mute fixture. Key-set-subset check retained in both cases.
grep -E '^\+.*\.(skip|only|todo)\(' over the full diff: no hits.
6. CI on d117857 (read 2026-09-08 ~08:48Z) · mergeable_state · distance from main
34 check runs on the head:
- completed / success (14): Build Docs · filter · Type Check · source gates · Auto Label · Part-of PR must not also close its card · No other open PR may claim the same single-writer path · Governed Surface Queue Guard · Spec property liveness · Check PR Size · Check Changeset · No other open PR may claim the same issue (×2) · Check Documentation Links · Flag docs affected by code changes
- completed / skipped (2): Console Pin Gate · Packed-tarball smoke (opt-in)
- completed / cancelled (2): earlier duplicates of "Part-of PR…" and "single-writer path" (runs
34205917707/34205917700, superseded by the successful re-runs above — not failures) - in_progress (16): Test Core (1–6/6) · Dogfood Regression Gate (1–3/3) · Build Core · Dogfood Verify CLI · Temporal Conformance (live PG + MySQL) · Lint & Repo Gates · Type Check · consumer gates · Type Check · debt ledger · Type Check · workspace
- failure: 0 at review time.
mergeable_state: blocked — the PR is draft: true, required checks are still running, and no approving review exists; no merge conflict is reported. Head is 2 commits behind origin/main (base sha d4401f7; the PR merged main at 68c74383f, then two more landed upstream — per the dev report, scripts/check-regen-pending.mjs / scripts/git-merge-regen.mjs, not files this diff derives a verdict from).
Not measured locally by the author and declared to CI: @objectstack/client full pnpm test / check:test-typecheck, check:dual-build-cjs-loads, check:type-check-debt. The client change is a type-only widening plus a docblock; the "Type Check · consumer gates" / "workspace" jobs are the ones that will answer it. The CI-conditional in the verdict is specifically these.
Findings
F1 — X-ObjectStack-Dropped-Fields header asymmetry on the clone route · Severity: Low / advisory — no change required in this PR
Evidence: packages/rest/src/rest-server.ts:8872 (res.status(201).json(result), no header) vs :8617 (create) and :8759 (update) which call applyDroppedFieldsHeader. After this PR the clone 201 body carries droppedFields while the route sets no header; POST /data/:object and PATCH /data/:object/:id set both; bulk routes set neither.
Judgement: not a defect. No published text promises the clone route a header (CreateDataResponseSchema's describe scopes the header sentence to itself; http-protocol.mdx names the body as the primary channel), and this PR's schema describe, data-api.mdx, client docblock and changeset all state body-only, so no reader is misled. Adding the header is a wire change the ruling did not name and would be out of scope here. Expectation: if header parity across single-record write routes is wanted, it is a separate card for the maintainer (one line: "should the clone route call applyDroppedFieldsHeader like create/update?"). Recording it so the asymmetry is not rediscovered; the PR author already flagged it.
F2 — Pending changeset tidy-cups-smile.md (PR #15395) will contradict this one in the same release · Severity: Info — no change required
Evidence: .changeset/tidy-cups-smile.md:14 — "cloneData forwards whole but reports nothing on the wire: its response contract (CloneDataResponseSchema, declared as produced) has no droppedFields member … adding that key is a spec change, not part of this one." Both files are unreleased and will be consumed by the same version packages.
Judgement: leaving it is correct — it is the accurate record of that change, and this PR's changeset states the newer fact with its date and ruling; the CHANGELOG reads as history. Editing another PR's landed changeset would be widening. Expectation: whoever compiles the release notes (content/docs/releases/, release-owned) reconciles the two sentences; no action on this PR.
F3 — objectql matches the LEVEL-axis regex but has no changeset entry · Severity: Info — correct as is
Evidence: packages/objectql/src/protocol-data.test.ts matches PUBLISHED_SOURCE_PATH; .changeset/clone-data-dropped-fields.md names no @objectstack/objectql. The gate's predicate (check-changeset-no-major.mjs:945) only flags a patch entry on a grown package, so an absent entry is not red, and the change is test-only (nothing ships). Recorded so the next reader does not "fix" it by adding a spurious entry.
No CHANGES REQUIRED finding. Ruling implemented exactly; the three out-of-ruling edits are owed by falsified published claims/pins, not widening; no governed path; changeset levels correct on the launch-window guard, the LEVEL axis and WHICH LEVEL; ADR-0087 not engaged. Verdict stands PASS WITH FINDINGS conditional on the 16 in-progress check runs finishing green.
Head reviewed: d117857520a6d6037dd8e1b4627b145b4a0eb390 · merge-base 30b0990787ef633cfff724951ceeee08c4fc05b9 · this seat wrote nothing else on GitHub and touched no checkout; the throwaway ref is deleted.
Generated by Claude Code
State note — readied by another seat; this seat's at-tier verdict is still outstanding. ⛔ Not enqueued.
Observed at 08:53Z, stated as measured rather than inferred:
⇒ This seat reads the stripped gate label as a clearance, not a fault — 「闸门标签被剥不是红灯是放行」 — so it is ⛔ not re-hung, and the ready flip is left standing. ⛔ No objection is being raised to either action. What is nonetheless still outstanding, and why this note exists: an at-tier contract review of this PR is running now and has not returned. ⇒ ⛔ This seat will not flip auto-merge or enqueue until that verdict lands, and asks that no other seat do so on the strength of The reviewer is specifically checking the dev's most consequential claim, which decides whether this PR's central act was meaningful: the card's named conformance pin ( Also awaiting a ruling from that review, raised by the dev and ⛔ deliberately not decided here: the clone route now carries I will post the verdict here when it returns, and act on it then. https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x Generated by Claude Code |
Addendum — correcting the sequence in my previous comment. ⛔ No objection; auto-merge left armed.
My note at 08:55Z asked that no seat arm auto-merge on the strength of ⛔ This changes nothing about what this seat does. Auto-merge stays armed and is not being disabled — another seat cleared the gate label and armed it, and this seat ⛔ does not override a legitimate action by another seat on a PR that is not governed. The at-tier review this seat is running is additional rigour, not a repo gate. ⇒ The honest statement of where that leaves things:
⛔ Nothing here asks anyone to reverse anything. https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x Generated by Claude Code |
⛔ Correction to my 08:54Z note — a PASS was already on the record. Plus this seat's independent at-tier verdict: PASS.
The correction, first, because it is mine and it was publishedMy comment Comment ⇒ I asserted an absence without reading this PR's comments. I read the labels and the PR state object, saw The consequence I most want to withdraw: my note implied the ready flip and auto-merge happened ahead of a review. They did not. The actual sequence is exactly the right one:
⇒ The gate was cleared because it had been satisfied, in the correct order. ⛔ No seat acted ungated, and my note should not be read as suggesting otherwise. This seat's independent verdict: PASSRun at tier (fuse: 95 harness-stamped Where this review adds measurement rather than repeating:
On F1 (the header asymmetry), where both reviews agreeThe clone route carries ⇒ This seat will file it as a low-priority card so the asymmetry is not rediscovered a third time — ⛔ not as a change to this PR. ⛔ Nothing about this comment asks anyone to reverse anything. Auto-merge stays armed; this PR merges when its checks finish green. https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x Generated by Claude Code |
Fixes #15703
Clause-②: yes— this PR widens a published response contract (CloneDataResponseSchema,@objectstack/spec/api, declared AS PRODUCED under the 2026-08-25 ruling on #11924) and is dispatched at the contract-review tier for that reason.needs:contract-reviewis applied.Ruling executed
Comment
5578819560on #15703 (director seat, decision batch #83, 2026-09-08, maintainer verbatim 「其他同意」) — option 1, four parts, all in this PR:CloneDataResponseSchemagains an optionaldroppedFieldsmember of the same Zod shape asCreateDataResponseSchema's (z.array(DroppedFieldsEventSchema).optional()).cloneDatapasses theonFieldsDroppedlistener exactly ascreateDatadoes (const dropped = []; opts.onFieldsDropped = (e) => dropped.push(e); spreaddroppedFieldsonto the return only when non-empty).packages/metadata-protocol/src/search-clone-schema-conformance.test.tsand PR fix(objectql,metadata-protocol): a staticreadonlyfield is stripped from a non-system INSERT insideengine.insert, and the boundary copy is deleted #15395's firing-control pin inprotocol.readonly-insert.test.tsflip in the same PR.@objectstack/specminor.Sequencing honoured: the member and the producer land together — the conformance suite measures the produced member on the real producer, not a hand-built body.
What changed
packages/spec/src/api/protocol.zod.tsCloneDataResponseSchema.droppedFields(optional, same element schema asCreateDataResponseSchema); docblock rewritten from "the producer emits nodroppedFields" to the ruling. The.describe()deliberately does NOT copyCreateDataResponseSchema's sentence about theX-ObjectStack-Dropped-Fieldsheader — measured: the clone route relays the body verbatim (res.status(201).json(result),packages/rest/src/rest-server.ts) and never callsapplyDroppedFieldsHeader(only the single create at ~8608 and single update at ~8750 do), so the describe says "body only, no header".packages/metadata-protocol/src/protocol.tscloneDatawires the listener and spreadsdroppedFields— byte-for-byte thecreateDatapattern; the strip itself is unchanged and stays the engine's.search-clone-schema-conformance.test.tsreadonlycolumn and the engine double now strips + firesonFieldsDropped(a double that never dropped would leave the produced branch vacuously green). Pin 2 flips from "nodroppedFields, by name" to "declared AND produced" (event asserted,safeParseraw and over the wire, key set still a subset of the declaration); a new third case pins omit-when-empty on the no-drop body. Pin 1 (no-drop body parses) unchanged.protocol.readonly-insert.test.tsapproval_statusand theoverrides-forgedsourcein the engine's one event; the firing-control enumeration grows to 7 inserts (cloneData included); the "cloneData is the one face that passes NO listener" case is replaced by a named presence pin. Header comment updated.packages/objectql/src/protocol-data.test.tsforwards context to findOne and insertassertedinsertwas called with EXACTLY{ context }; now{ context, onFieldsDropped: expect.any(Function) }.packages/client/src/index.tsCloneDataResult(declared mirror ofCloneDataResponseSchema, return type ofclient.data.clone) gains the same optionaldroppedFields?: DroppedFieldsEvent[]. Beyond the three ruled files, and flagged for the reviewer: its published docblock said in as many words "NodroppedFields: the clone producer emits no write-observability event" — a sentence this PR makes false in a shipped.d.ts, so it is fixed here under the "a published claim your change falsifies is yours" rule. Drop it if the seat wants the PR strictly at the ruled three; nothing else depends on it..changeset/clone-data-dropped-fields.md@objectstack/specminor (ruled);@objectstack/metadata-protocolminor (the 201 body gains a member);@objectstack/clientminor (published type widened). Not declared breaking — no ADR-0087 marker owed.packages/spec/authorable-surface/api.json,content/docs/references/api/protocol.mdxcheck:generated --fix(only the docs were proved stale; the surface shard is written bygen:schemainsidebuild).authorable-surface.base.jsonuntouched.Premises measured (ZONE 2), against
47f751d5d(origin/main at branch time)readonlyfield is stripped from a non-system INSERT insideengine.insert, and the boundary copy is deleted #15395 is merged (merged_at: 2026-09-05T12:24:23Z, merge commitb398ad258, inorigin/main's log); its firing-control block is atprotocol.readonly-insert.test.ts:254-267onmain. Flipping it fights nothing. Holds.CloneDataResponseSchemawas exactly{ object, id, sourceId, record }(protocol.zod.ts:2007-2017). Holds.expect(body).not.toHaveProperty('droppedFields'), plus the key-set-subset check at 221-223. Nuance worth the reviewer's eye: that forbid was measured on a fixture engine that never firedonFieldsDropped, so it was vacuous with respect to the listener — wiring the listener alone would NOT have turned it red. The real teeth against wiring were elsewhere:protocol.readonly-insert.test.ts:266(onFieldsDroppedtoBeUndefined) andpackages/objectql/src/protocol-data.test.ts:654(exact options{ context }), the latter not named on the card. "Flipping" the conformance pin therefore meant giving the fixture a readonly column and a double that fires the event, so the produced branch is measured — not merely deleting a negation.createData,createManyData,insertManyData,batchDatacreate rows and both creating upsert arms all passonFieldsDroppedand surfacedroppedFields(the existing firing-control test enumerates 6 insert call sites, all with a listener);cloneDatapassed onlyctxOpt. Holds.CreateDataResponseSchema's shape — read, not inferred:droppedFields: z.array(DroppedFieldsEventSchema).optional().describe(...). Copied as the Zod shape; the describe prose differs only where the fact differs (the header, see above). Holds.cloneDatapasses no listener today —this.engine.insert(request.object, data, ctxOpt as any),ctxOptis{ context }or undefined. Holds.Verification
Filled in from the run record — see the report comment on #15703 for the full command/exit table. Summary at the head sha this body names:
Every build/test/typecheck ran through
scripts/pm/os-verify-lock.sh; every exit was captured before any pipe (cmd > log 2>&1; ex=$?). Two heads becauseorigin/mainwas merged in after the first round (AGENTS.md §10); the hand-written delta is byte-identical across the merge (diff-of-diffs withcmp), andpackages/specmoved on both sides, so its full suites were re-run on the merged head.b617ce886and again at68c74383fpnpm --filter @objectstack/spec buildVERDICT command-exit 0(204s / 209s); rebuiltdist/api/index.d.tscarriesdroppedFieldsinsideCloneDataResponseSchema(positive control: 15droppedFieldshits in the same file)b617ce886then68c74383fpnpm --filter @objectstack/spec check:generated(then--fix)content/docs/references/**→gen:docs), regenerated and committed; post-merge:✓ All 15 generated artifacts are up to date.b617ce886turbo run build --filter='@objectstack/objectql^...' --filter=@objectstack/metadata-protocol --filter='!@objectstack/spec' --concurrency=2metadata-protocol/dist/index.jscarries the spreaddroppedFieldsat the clone returnb617ce886pnpm --filter @objectstack/metadata-protocol testTest Files 169 passed | 2 skipped (171),Tests 2434 passed | 10 skipped (2444)b617ce886pnpm --filter @objectstack/metadata-protocol typecheck(tsc --noEmit)error TSlines68c74383fpnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 src/search-clone-schema-conformance.test.ts src/protocol.readonly-insert.test.tsTest Files 2 passed (2),Tests 19 passed (19)b617ce886and68c74383fpnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/protocol-data.test.ts src/protocol-clone-real-engine.test.tsTest Files 2 passed (2),Tests 121 passed (121)both timesb617ce886pnpm --filter @objectstack/objectql run check:test-typecheckcheck:test-typecheck: OK — @objectstack/objectql's test layer compiles … 44 file(s) / 242 error(s) / 69 pinned signature(s) held(the ledgered debt, unchanged)b617ce886and68c74383fpnpm --filter @objectstack/client exec tsc --noEmitb617ce886and68c74383fpnpm --filter @objectstack/spec testTest Files 465 passed (465),Tests 12955 passed (12955)both timesb617ce886and68c74383fpnpm --filter @objectstack/spec typecheckcheck:test-typecheck: OK — … 54 file(s) / 261 error(s) / 145 pinned signature(s)(ledgered, unchanged)NOT MEASURED locally, declared to CI:
@objectstack/client's fullpnpm testand itscheck:test-typecheck— its unit tests import@objectstack/runtime/@objectstack/objectql/@objectstack/plugin-hono-serverunaliased (dist), which are outside the closure built here; the client change is a type-only widening ofCloneDataResult, andclient.data.clone's binding pin (return-type-precision.test.ts) is type-level. Alsopnpm check:dual-build-cjs-loadsandpnpm check:type-check-debt— both answeredPREREQUISITE NOT MET(exit 3: 57 packages withoutdist/); a refusal, not a pass.Gate sweep (lock-free, on the merged head
68c74383f):node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 103 families from the merge-base change set (the pre-merge derivation was 80, then a STALE TREE warning named 8 gate scripts moved upstream — the reasonorigin/mainwas merged before this PR opened rather than trusted or ignored). All 103 ran, each exit captured before any pipe;--ranreconciles103 derived, 103 run, 0 UNRUN. 101 exit 0 — includingcheck:nul-bytes,check:empty-changeset,check:adr-0087-registration,check:changeset-no-major,check:pm-widening-tells,check:engine-double-contract, speccheck:api-surface/check:authorable-surface/check:docs/check:liveness/check:skill-examples(the last measured only after@objectstack/clientand@objectstack/client-reactwere built — its first run was a build-prerequisite refusal). The 2 non-zero are the exit-3 refusals named above (NOT MEASURED, not red).check:react-declaration-parityisEXTERNAL_INPUT_REQUIREDhere as always.Ablation (the listener wiring deleted, everything else intact)
Script:
scratchpad/issue-15703/ablation.sh(not committed), run under the lock atb617ce886(VERDICT command-exit 0 · held 58s),trap restore EXIT INT TERMwith absolute paths. Both legs proven on disk, never by an editor's exit code.Mutation: in
cloneDataonly,const opts: any = { onFieldsDropped: … }becomesconst opts: any = { /* ABLATION_15703_NO_LISTENER */ }— proven bygrep -c: marker 1, the neighbouringopts.contextline still 1, the three identical listener spellings in the other faces untouched (count 3).src/(this package's tests import./protocol.js)vitest run --maxWorkers=2 src/search-clone-schema-conformance.test.ts src/protocol.readonly-insert.test.tsTests 4 failed | 15 passed (19):cloneData forwards the copied row AND the caller overrides whole — and reports the engine's verdict on both(expected undefined to deeply equal [ { object: 'approval_case', … } ]);every create face whose response carries droppedFields passes an onFieldsDropped listener(expected 'undefined' to be 'function');cloneData passes the listener too — the sixth face(same);emits no top-level key the spec does not declare — droppedFields included, now that it is declared AND produced(expected undefined to deeply equal [ { object: 'customer', … } ])dist/(objectql's tests import@objectstack/metadata-protocolunaliased — it is inKNOWN_UNALIASED_TEST_IMPORTS)pnpm --filter @objectstack/metadata-protocol build(exit 0) →node scripts/ablation-dist-preflight.mjs @objectstack/metadata-protocol ABLATION_15703_NO_LISTENER(exit 0, marker present in dist) →vitest run --maxWorkers=2 src/protocol-data.test.tsTests 1 failed | 116 passed (117):cloneData › forwards context to findOne and insert(expected "vi.fn()" to be called with arguments: [ 'account', Anything, { …(2) } ])git checkout HEAD -- ABS_PATH(absolute path fromgit rev-parse --show-toplevel)git diff HEAD0 lines;git hash-object= HEAD blob67bcd138cc046aaf053bbe61ad23956b70e80a52(equal); marker count 0;git status --porcelainemptydist/pnpm --filter @objectstack/metadata-protocol build(exit 0) → preflight--absentmarker absent from all 24 built files,working tree clean against HEADsrc/anddist/Tests 19 passed (19); objectqlTests 117 passed (117)Direction observed: red, as expected for a deleted producer — no reversal, no "more diagnostics" shape. The green suites prove the pins exist; the red legs prove they bind to exactly the listener wiring.
验收备注 (acceptance notes — observations, not filed)
X-ObjectStack-Dropped-Fieldson the clone route — after this PR the clone's 201 body carriesdroppedFieldsbut the route sets no header, whilePOST /data/:objectandPATCH /data/:object/:idset both (bulk routes set neither). Not a defect class (a missing member is incompleteness, not an error, and no contract text promises the clone route a header), and deliberately not added here: it is a wire change the ruling did not name and the tier reviewer may want to rule on. The schema describe states the body-only fact so no reader is misled. 承接者: the contract reviewer of this PR, if they want it; otherwise none..changeset/tidy-cups-smile.md(PR fix(objectql,metadata-protocol): a staticreadonlyfield is stripped from a non-system INSERT insideengine.insert, and the boundary copy is deleted #15395's pending changeset) still says "cloneDataforwards whole but reports nothing on the wire … adding that key is a spec change, not part of this one". Left untouched — it is the accurate record of THAT change; this PR's changeset states the new fact. When both release together the CHANGELOG will read as history. 承接者: whoever compiles release notes.docs/qa/platform-checklist/areas/records-forms.jsonitem "Clone contract edges" describes the body as{object, id, sourceId, record}; still true for a no-drop clone, and the item's readonly-forge step could now additionally assertdroppedFields. 承接者: the nextchecklist-authorsweep.ctxOptconsumers? No —ctxOptis still used forfindOne; only the insert call moved toopts.Docs drift (hand-written pages the drift bot listed — read and decided, not regenerated)
Measured on this branch: none of the five listed pages names
CloneDataResult(0 hits each; control:content/docs/references/api/protocol.mdxnamesCloneDataResponse4 times), so the bot bridged the bare keydroppedFields, not the client type — there is no "same type / different type" question to settle on those pages.content/docs/api/data-api.mdx{ object, id, sourceId, record }— a sentence this PR falsifies. Now{ object, id, sourceId, record, droppedFields? }, with when it appears (a copied or overriddenreadonlycolumn), what it means, and that it rides the body only (noX-ObjectStack-Dropped-Fieldsheader on this route).content/docs/kernel/contracts/data-engine.mdxonFieldsDroppedas their responsedroppedFieldswherever their contract declares one". True before (cloneData's contract declared none) and true after (it now declares one and the producer fills it) — the hedge simply covers one more face. Not falsified; tightening it would be polish.content/docs/protocol/kernel/http-protocol.mdxdroppedFieldsonly as the generic body channel behind the exposedx-objectstack-dropped-fieldsheader ("the response body'sdroppedFieldsstays the primary channel"). Route-agnostic and still true; for the clone route the body is the only channel, which the schema describe anddata-api.mdxnow say.content/docs/releases/implementation-status.mdxcontent/docs/releases/v17.mdxdroppedFieldsforPATCH/POST /data, bulk, batch and the SDK; it never claimed or denied the clone. Not falsified — no docs-only card needed.packages/spec/authorable-surface/api.jsonyielded no anchor for that check, so its coverage there is not a clean bill of health; its own gate (check:authorable-surface) is green on this branch and the diff to it is the single added keyapi/CloneDataResponse:droppedFields.Generated by Claude Code