fix(metadata-protocol): make the destructive-change remedy clause face-aware — stop prescribing ?force=true on the duplicate door (#11015) - #11099
Conversation
…e-aware (#11015) `saveMetaItem`'s Phase 3a-destructive refusal ended every message with `— re-submit with ?force=true to proceed.` The refusal is raised in one place and quoted onto whatever response the caller's catch builds, so that sentence went out on every face — including `POST /packages/:id/duplicate`, which accepts no `force` in the query string or the body, and whose `duplicatePackage` request type has no `force` field for the internal `saveMetaItem` call to carry. The clause is now rendered per face. `duplicatePackage` states the face on its internal call — server-stated, exactly as `source` already is — and gets the remedies that exist there: a free target namespace, or reconciling the collision first. Faces that state no door keep the previous wording byte for byte. Repairs the CLAUSE, not the door: no `force` is added to the duplicate route. #10886's verdict is untouched — the per-field findings prose stays, because `failed[].error` is its sole carrier on that face. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
…paired faces (#11015) The docblock and the face-inventory header both recorded that inventory rows 2 and 3 carry the same wrong remedy and were knowingly left. Point them at the issue that now holds that measurement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
📓 Docs Drift CheckThis PR changes 1 package(s): 19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 7 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 2cbc698f5e67055ee66eb26c6657679ae1aa4c4b && git checkout 2cbc698f5e67055ee66eb26c6657679ae1aa4c4b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab47f6974a275586355f06abd83a0975b8637d12 c8b63f34f9604785764f7865bc7d340d8d723dac && git checkout -B drift-repro ab47f6974a275586355f06abd83a0975b8637d12 && git merge --no-ff c8b63f34f9604785764f7865bc7d340d8d723dac
node scripts/docs-audit/affected-docs.mjs --json ab47f6974a275586355f06abd83a0975b8637d12
|
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 32586810427 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Fixes #11015
saveMetaItem's Phase 3a-destructive refusal ended every message with— re-submit with ?force=true to proceed.The refusal is raised in one place and quoted onto whatever response the caller's catch builds, so that sentence went out on every face that reaches the gate — includingPOST /packages/:id/duplicate, where there is noforceto set. A caller who does what the sentence says gets the identical refusal back.Premise re-verified on current
origin/main(4257e4e)The card measured on
490879ad0f; re-derived here, with two line drifts:targetPackageId/targetName/targetNamespace/organizationId/actor, noforcepackages/runtime/src/domains/packages.ts:832, the line the card namesduplicatePackage's request type has noforcefieldprotocol.ts:16557saveMetaItemcall is aroundprotocol.ts:16807:16844protocol.ts:13210, not near:16807new Error(...)in the Phase 3a-destructive gateReachability re-confirmed: a plain duplicate re-namespaces every object, so the target name usually does not exist and the gate is skipped. It fires on the duplicate-again workflow, where the target namespace already holds the renamed object.
The repair
The clause is rendered per face by
destructiveChangeRemedy(face, name).duplicatePackagestates its face on the internalsaveMetaItemcall, and gets the remedies that exist there:Faces that state no door keep the previous wording byte for byte —
PUT /api/v1/meta/:type/:namereads?forceand threads it, so?force=trueis right there.Three lines this deliberately does not cross
forceis added toPOST /packages/:id/duplicate. That would widen a public surface and is a contract decision above this dispatch. The signal is server-stated, exactly as the existingsourcefield is:duplicatePackagehard-codes it, its own request type has no such field, and a test drivesduplicatePackage({ force: true, writeFace: … })from the outside to pin that a caller cannot smuggle one in.issuescarries — the remaining duplication of the #10524 family, different refusal class #10886's verdict is untouched.failed[].erroris the sole carrier of the per-field destructive findings on this face, so the findings prose stays verbatim. Only the trailing remedy sentence is face-dependent, and a test asserts the prose is still there.duplicatePackage的failed[]是否应在 spec 声明结构化issues通道 —— 它今天在 spec 里没有任何响应 schema #11017. The wording states remedies and nothing else — it neither assumes a structuredissueschannel onfailed[]nor asserts there will never be one, so it reads correctly under either outcome. Nopackages/specchange:SaveMetaItemRequestSchemadeclares only{ type, name, item }and is not touched;writeFacejoinsforce/source/mode/packageIdon the implementation's inline request type, which has always been wider than the schema.Verification — all at
c8b63f34fAblation — direction predicted before running. Mutation: revert only the producer (
protocol.ts) toorigin/main, keeping the new tests. Predicted the four remedy-clause assertions go RED while the behaviour and prose assertions stay GREEN — a discriminating split, since an all-red result would mean the fixture tests something broader than the clause. Measured exactly that: 4 failed | 9 passed.RED: section 3's replaced assertion ·
never prescribes force·prescribes the remedies that DO exist·the face is stated by the SERVER.GREEN:
[#10886 non-effect] findings prose still there·the refusal still REFUSES·OTHER faces keep the ?force=true wording· all four #10886 structural pins.No rebuild leg is owed and this is not the dogfood exemption being assumed:
packages/metadata-protocol/distdid not exist for the whole run, so vitest could only have resolved the relative./protocol.jsspecifier tosrc/protocol.ts. The mutation was confirmed on disk by anchored counts, not by an editor's exit code —destructiveChangeRemedyrefs3 → 0, old clause literal0 → 1— and the script carriedtrap … EXIT INT TERM; the restore leg was confirmed the same way (3/0, clean tree) and re-run green.One stale fixture, replaced not re-spelled.
protocol.destructive-409-face-inventory.test.tsassertedexpect(r.failed[0].error).toContain(REMEDY)on the duplicate face withREMEDY = 're-submit with ?force=true to proceed.'. It passed because of the branch this PR changes — it was pinning the defect. Replaced with a pin of the corrected remedy; what #10886 put it there to protect (some remedy reaches the caller through this string and nothing else) is unchanged and still asserted.Suites. Full
@objectstack/metadata-protocolsuite:Test Files 134 passed | 2 skipped (136),Tests 1844 passed | 10 skipped (1854). Package build (tsup, dts on): exit 0 — soprotocol.tstypechecks under declaration emit.Gates — 20 run, all green at
c8b63f34f, exit codes captured before any pipe. Re-derived withnode scripts/pm/dispatch-gates.mjs(no paths passed) rather than from the dispatch list, which surfaced five convention-triggered families the dispatch list did not name —check:query-options-erasure,check:type-check-coverage,check:type-check-debt,check:engine-double-contract,check:where-matcher— because this PR edits a test file. All but the debt ratchet were run and are green:check:cross-package-test-inputs·check:durability-log-level·check:filter-alias-parity·check:slot-lookup·check:test-source-alias·check:type-source-resolution·check:changeset-gate-self-tests·check:objectui-changeset·check:query-options-erasure·check:engine-double-contract·check:where-matcher·check:type-check-coverage·check:nul-bytes·check-adr-0087-registration·check-changeset-no-major·check-ci-filter-parity·check-cross-package-test-inputs·check-empty-changeset·check-plugin-teardown-shape·docs-audit/check-affected-docsTwo declared narrowings
check:type-check-debtneeds the whole workspace built and refuses on an unbuilt worktree. Measured its content directly instead, with the gate's own invocation form:tsc --noEmit -p packages/metadata-protocol/tsconfig.jsonreports 63 errors against a ledger entry recording 63 (scripts/check-type-check-coverage.mjs:530), and none of the 63 is in either file this PR touches — all sit in 14 untouched test files. The ratchet re-measures per ledger entry and this diff touches exactly one package, so no other entry's count can move.pnpm lintis the repo-wide scan CI owns. Narrowed to the changed files, with the evidence that makes the narrowing a measurement rather than a skip: ① the population came from eslint resolving the paths under its own config, not from a guess about which files count; ②--format jsonreported 2 files linted, 0 errors, 0 warnings; ③ the config enables no type-aware linting — noparserOptions.project, no typed@typescript-eslintrules, stated as an invariant ateslint.config.mjs:328— so this diff cannot move the verdict on any file it does not contain.Filed, not fixed
The same inventory says rows 2 and 3 —
@objectstack/rest's compound-namePUT /meta/:type/:a/:b(rest-server.ts:6590) and@objectstack/runtime's dispatcherPUT /meta(domains/meta.ts:417) — reach the same gate and also never threadforce, so they carry the same unactionable sentence. Filed unassigned as #11095 rather than folded in: unlike the duplicate gesture, those doors have no collision-free alternative to prescribe, so the honest repair may be to threadforceon them — a contract decision, not a message fix.#11095 remains open. The docblock and the test header both name it.Generated by Claude Code