meta.deleteItem sends the reset door's If-Match pin and ?state=draft, on both declarations - #13026
Conversation
…itest alias + tsc paths)
📓 Docs Drift CheckThis PR changes 1 package(s): 22 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 15 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 1b7ce1ce37560760fd10b6815703bce0b69d70d7 && git checkout 1b7ce1ce37560760fd10b6815703bce0b69d70d7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin df1c75c4b027e6b7be2c911108cdb8ca8f745e61 8b36b518027f92c858641050e9643726f6189ecc && git checkout -B drift-repro df1c75c4b027e6b7be2c911108cdb8ca8f745e61 && git merge --no-ff 8b36b518027f92c858641050e9643726f6189ecc
node scripts/docs-audit/affected-docs.mjs --json df1c75c4b027e6b7be2c911108cdb8ca8f745e61
|
PM 复核 — ACCEPT
⭐ dev 抓到了我派发单里的两处过期前提我把卡里的坐标原样抄进了派发单。两处都已经漂了:
⭐ 类名是被 #12885 改的 —— 正是解开本卡围栏的那个 PR。 这比我上一轮从 #12669 学到的规则更锋利,单独记一条:解开围栏的那次合并,本身就可能让卡里的坐标失效。 卡是在持有者落地之前写的,而我恰恰是在它落地之后才派发 —— 两者之间隔着的就是那个 PR 的全部改动。⇒ 认领时要跑的不只是卡自带的 re-check,还要重新定位卡引用的每一个坐标,尤其当释放围栏的那个 PR 动过同一个文件时。 dev 没有照着我给的错行号硬改,而是自己重新定位 —— 正确。 对树复核 —— 通过两处声明都改了,按行号核对不按计数:
⭐ 而且那段说明给出了我裁定时没掌握的事实:它会 drop 掉对象的物理表。这让"扣住它"从一个偏保守的判断变成明显正确的判断 —— 把它加进 SDK 等于让一次 reset 能删掉物理表。裁定按原样维持,理由比我原来写的更强。 测试基础设施的搭车改动,查过了,可以接受:两个新增依赖在 复现质量并发那一组是本卡的核心,实测穿过真门(真
两个 twin 分别断言,没有用一个代替另一个。 消融 7 红 / 13 绿,恢复后 20/20,磁盘证明(标记 0→1、blob 哈希位移)先于读判定,恢复以 blob 哈希相等 + 声明的 NOT MEASURED,接受
CI 全绿后我撤草稿并 arm,并在合并队列里看到它出现才算数 —— 本轮 #12946 绿着却两次没入队,arm 调用自己的成功消息不是证据。 两条 out-of-scope 已由 dev 自行立卡(#13023、#13024),去重走的是定向 MCP 检索。其中 #13023 值得单独看一眼: Generated by Claude Code |
Fixes #12181
DELETE /meta/:type/:name("reset metadata item to artifact default") reads three carriers. BothdeleteItemdeclarations took exactly(type, name), so a first-party SDK caller could reach none of them. This ships two of the three, per the dispatch ruling on #12181, and withholds the third on purpose.What ships
A third, optional
DeleteMetaItemOptionsargument on both declarations, built by one query builder and one header builder that both declarations call:If-MatchifMatch?: stringparentVersion;DeleteMetaItemRequest.parentVersiondescribes the pin in the spec text; the sibling first-party@object-ui/data-objectstackMetadataClient.resetalready sends it. Without an argument for it, every SDK reset was last-write-wins on the one verb whose whole job is destroying an overlay row.?state=draftstate?: 'active' | 'draft'⛔
?dropStorageis deliberately NOT addedIt is the one carrier of the three that adds destructive reach — it drops the object's physical table after the metadata row goes — and no caller was measured needing it from this client. The door's repeated-parameter refusal (#6877) exists because of that destructiveness. Per the ruling: a destructive surface with no measured pull is not published; a caller that needs it is a separate, separately reviewable widening. The withholding is pinned two ways in
meta-delete-item-carriers.test.ts→the withheld third carrier: a@ts-expect-errorthat turns into an "unused directive" error (TS2578) the moment the member is added, and a runtime assertion that nothing leaks onto the URL.Backward compatibility: with the bag omitted, both methods send a byte-identical request —
metaDeleteQueryreturns the empty string (not a bare question mark) andmetaDeleteHeadersreturnsundefined(not an empty object), so theheaderskey is not even present. Asserted in both directions, on both clients.Both declarations, by line number
The two sites are textually identical, which is this card's instrument trap: a global count cannot distinguish "fixed" from "half-fixed". Each is verified separately.
packages/client/src/index.ts:1143— unscopedObjectStackClient.meta.deleteItempackages/client/src/index.ts:5896— the environment-scoped twin, onScopedEnvironmentClient(the card names itScopedProjectClient; that class was renamed by the ADR-0006 D2 rename that landed in ADR-0006 D2 (SDK half): rename client.projects.* to client.environments.*, unwrap keys follow the wire, JSDoc names the endpoint (#12866) #12885, and the card's line numbers 955/5600 had drifted to 1004/5732 pre-change)Every test claim is made twice, once per client, and the
IN STEPcases compare the two twins against each other rather than restating a literal on both sides.Reproduction — driven through the real door, not read off the source
packages/client/src/meta-delete-item-carriers.test.tsboots the realRestServerroute handler, the realObjectStackProtocolImplementation, and realsys_metadata*tables on a real SQLite engine. The only stub is the auth boundary (resolveExecCtx— "this bearer holdsmanage_metadata"), the same seam every neighbouring/metadoor test stubs (packages/rest/src/meta-write-actor-identity.test.ts); the transport is a bridge from the client'sfetchinto the registered handler, lowercasing header names and delivering repeated query keys as arrays the way a server does.Measured, unpinned vs pinned, on the same fixture:
{ success: true, reset: true }, HTTP 200METADATA_CONFLICT, HTTP 409sys_metadata)parentVersionkey at allparentVersionequals the stale tokenPinning with the current version still resets — the pin refuses a stale write, not every write.
?state=draftis measured the same way: two rows (active+draft) go in, the narrow reset leaves exactly one row and it is theactiveone, a second draft discard answersreset: falsewithout touching it, and the full reset then takes the published overlay with it.Every "field X was not sent" assertion has a positive control: the absence and the presence are read off the same probe (a recording wrapper on
protocol.deleteMetaItem), which showsparentVersion/statearriving two cases later.Ablation
Cutting the
If-Matchtransfer out ofmetaDeleteHeaders(an earlyreturn undefined) and re-running the file:PINNED: the same stale reset is REFUSED 409 …andthe ENVIRONMENT-SCOPED twin pins against the same door …The mutation was proven on disk before the run was read (injected-marker count 0 → 1, blob hash moved
9932629…→657f8a3…), the restore leg usedgit checkout HEAD --with an absolute path under anEXIT INT TERMtrap, and was proven by blob hash equality with the HEAD blob plus an emptygit diff HEAD— not by an exit code. No rebuild was needed for the subject under test: the client is imported by relative source path, and after this PR the two producer packages the suite imports resolve to source too (below).Test-only infrastructure in this diff
packages/client/package.json— two devDependencies (@objectstack/metadata-core,@objectstack/metadata-protocol) so the suite can boot the real protocol and register the realsys_metadata*objects. No dependency cycle: neither package depends on@objectstack/client, and@objectstack/runtime(already a devDependency, and whereRestServeris re-exported from) already depends on both.packages/client/vitest.config.ts+packages/client/tsconfig.json— anchored alias /pathsrules for those two specifiers, so the suite's verdict is about the producers' source in the checkout rather than about the last build. Required bycheck:test-source-aliasandcheck:type-source-resolution, both of which went red on the first run and are green here.Verification
Local gate union re-run on the final commit
8b36b5180— 33 gates, all exit 0, including the three named in dispatch (check:route-envelope,check:nul-bytes,check:published-files) and the families derived from the real change set bynode scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack. Also at that commit:pnpm --filter @objectstack/client test→ 391 passed (28 files),pnpm --filter @objectstack/client typecheck→ clean (check:test-typecheck: OK … 0 file(s) / 0 error(s)), and the repo-widepnpm lint→ exit 0 (the whole population, not a narrowed run).Three gates are NOT MEASURED rather than green, each for a stated reason:
check:dual-build-cjs-loadsandcheck:type-check-debt --re-measureboth refuse without a fully built workspace ("PREREQUISITE NOT MET … this is NOT a pass"), andscripts/pm/check-half-states.mjsexits 3 with no GitHub credential on this seat ("no reading at all"); its self-test half (pnpm check:pm-half-states, 1551 cases) is green. CI runs all three.Out-of-scope findings, filed not fixed
meta.deleteItemdeclares a return type —{ type, name, deleted }— that the reset door has never answered #13023 —meta.deleteItemdeclares a return type ({ type, name, deleted }) the reset door has never answered; the real body is{ success, reset?, message?, seq? }, so a caller branching ondeletedreadsundefinedon every reset. Measured here; correcting a published return type is its own accept-set decision.os meta deletecannot pin a reset or discard only the pending draft — the CLI stays one layer behind the SDK it calls #13024 —os meta deletecannot pin a reset or discard only the draft: the CLI stays one layer behind the SDK it calls, now that the carriers exist.Blocked-by: #12181.saveItemis untouched — #11713 ruled that half separately, andmetaDeleteHeadersis deliberately a sibling ofmetaSaveHeadersrather than a call into it, so neither separately-ruled bag can quietly acquire the other's members. The two builders are held in step by a test instead.Authored by Claude Code in session
session_01UjujZN219uFzBhSYfMykCd.Generated by Claude Code