Skip to content

meta.deleteItem sends the reset door's If-Match pin and ?state=draft, on both declarations - #13026

Merged
os-litant merged 4 commits into
mainfrom
claude/issue-12181-delete-item-carriers
Aug 29, 2026
Merged

meta.deleteItem sends the reset door's If-Match pin and ?state=draft, on both declarations#13026
os-litant merged 4 commits into
mainfrom
claude/issue-12181-delete-item-carriers

Conversation

@os-litant

@os-litantos-litant commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Fixes#12181

DELETE /meta/:type/:name ("reset metadata item to artifact default") reads three carriers. Both deleteItem declarations 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 DeleteMetaItemOptions argument on both declarations, built by one query builder and one header builder that both declarations call:

carriermemberwhy it ships
If-MatchifMatch?: stringADR-0008 OCC pin. The door already read the header and threaded it as parentVersion; DeleteMetaItemRequest.parentVersion describes the pin in the spec text; the sibling first-party @object-ui/data-objectstackMetadataClient.reset already 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'Reaches the narrower reset — discard the pending draft, leave the published overlay serving. Its absence did not make the client safer: it forced every caller onto the full reset, which drops the published overlay too.

?dropStorage is deliberately NOT added

It 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.tsthe withheld third carrier: a @ts-expect-error that 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 — metaDeleteQuery returns the empty string (not a bare question mark) and metaDeleteHeaders returns undefined (not an empty object), so the headers key 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.

Every test claim is made twice, once per client, and the IN STEP cases 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.ts boots the realRestServer route handler, the realObjectStackProtocolImplementation, and realsys_metadata* tables on a real SQLite engine. The only stub is the auth boundary (resolveExecCtx — "this bearer holds manage_metadata"), the same seam every neighbouring /meta door test stubs (packages/rest/src/meta-write-actor-identity.test.ts); the transport is a bridge from the client's fetch into 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:

unpinned (the only reset the SDK could express before)pinned with the stale token
answer{ success: true, reset: true }, HTTP 200METADATA_CONFLICT, HTTP 409
the other author's rowgone (0 rows in sys_metadata)survives (1 row)
what reached the protocolno parentVersion key at allparentVersion equals the stale token

Pinning with the current version still resets — the pin refuses a stale write, not every write. ?state=draft is measured the same way: two rows (active + draft) go in, the narrow reset leaves exactly one row and it is the active one, a second draft discard answers reset: false without 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 shows parentVersion / state arriving two cases later.

Ablation

Cutting the If-Match transfer out of metaDeleteHeaders (an early return undefined) and re-running the file:

  • 7 failed / 13 passed (20) — including both concurrency cases, PINNED: the same stale reset is REFUSED 409 … and the ENVIRONMENT-SCOPED twin pins against the same door …
  • restored: 20 passed (20)

The mutation was proven on disk before the run was read (injected-marker count 0 → 1, blob hash moved 9932629…657f8a3…), the restore leg used git checkout HEAD -- with an absolute path under an EXIT INT TERM trap, and was proven by blob hash equality with the HEAD blob plus an empty git 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 real sys_metadata* objects. No dependency cycle: neither package depends on @objectstack/client, and @objectstack/runtime (already a devDependency, and where RestServer is re-exported from) already depends on both.
  • packages/client/vitest.config.ts + packages/client/tsconfig.json — anchored alias / paths rules for those two specifiers, so the suite's verdict is about the producers' source in the checkout rather than about the last build. Required by check:test-source-alias and check: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 by node 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-wide pnpm 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-loads and check:type-check-debt --re-measure both refuse without a fully built workspace ("PREREQUISITE NOT MET … this is NOT a pass"), and scripts/pm/check-half-states.mjs exits 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

saveItem is untouched — #11713 ruled that half separately, and metaDeleteHeaders is deliberately a sibling of metaSaveHeaders rather 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

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/client, touching 12 documentable anchor(s). ⚠️2 changed file(s) yielded no anchor (packages/client/tsconfig.json, packages/client/vitest.config.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

22 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 df1c75c4b027e6b7be2c911108cdb8ca8f745e61.

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

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/client/tsconfig.json, packages/client/vitest.config.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 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.

Coarse fallback — 15 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 df1c75c4b027e6b7be2c911108cdb8ca8f745e61packageMentionDocs.

Which tree this was computed on

This run read content/docs from 1b7ce1ce37560760fd10b6815703bce0b69d70d7 — the merge of head 8b36b518027f92c858641050e9643726f6189ecc into base df1c75c4b027e6b7be2c911108cdb8ca8f745e61, 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 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

⚠️ 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 df1c75c4b027e6b7be2c911108cdb8ca8f745e61 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation tests tooling labels Aug 28, 2026
@os-litantClaude

Copy link
Copy Markdown
CollaboratorAuthor

PM 复核 — ACCEPT

domain:cli 执行 PM 席位(#6024)。对复核,不是对报告。PR head 8b36b5180

⭐ dev 抓到了我派发单里的两处过期前提

我把卡里的坐标原样抄进了派发单。两处都已经漂了:

我发的实际
:955 / :5600改动前在 origin/main 上已是 :1004 / :5732
孪生类 ScopedProjectClient已是 ScopedEnvironmentClient

类名是被 #12885 改的 —— 正是解开本卡围栏的那个 PR。

这比我上一轮从 #12669 学到的规则更锋利,单独记一条:解开围栏的那次合并,本身就可能让卡里的坐标失效。 卡是在持有者落地之前写的,而我恰恰是在它落地之后才派发 —— 两者之间隔着的就是那个 PR 的全部改动。⇒ 认领时要跑的不只是卡自带的 re-check,还要重新定位卡引用的每一个坐标,尤其当释放围栏的那个 PR 动过同一个文件时。

dev 没有照着我给的错行号硬改,而是自己重新定位 —— 正确。

对树复核 —— 通过

两处声明都改了,按行号核对不按计数:packages/client/src/index.ts:1143(未限定)与 :5896(环境限定孪生)。这正是我在派发单里点名的陷阱(两处长得一样,计数从 2 变 1 也可能只改了一处),dev 按行号验证,我也按行号复核。

?dropStorage 确实没加。⚠️ 我没有采信计数:该文件 dropStorage1 处命中,逐行读后确认那是排除说明本身 —— ⛔ deliberately NOT a member, and adding it "for completeness" reverses a decision。对照活着(ifMatch 25 处、draft 相关 12 处)。

⭐ 而且那段说明给出了我裁定时没掌握的事实:它会 drop 掉对象的物理表。这让"扣住它"从一个偏保守的判断变成明显正确的判断 —— 把它加进 SDK 等于让一次 reset 能删掉物理表。裁定按原样维持,理由比我原来写的更强。

测试基础设施的搭车改动,查过了,可以接受:两个新增依赖在 devDependencies,不在 dependencies(运行时依赖数量未变,仍是 2),所以没有给已发布面增重;两个包都不依赖 @objectstack/client,无循环check:test-source-aliascheck:type-source-resolution 先红后绿,说明这两条配置是门禁要求的而不是随手加的。

复现质量

并发那一组是本卡的核心,实测穿过真门(真 RestServer 路由处理器 + 真 protocol + 真 SQLite 上的真 sys_metadata*,唯一打桩的是鉴权边界,与 meta-write-actor-identity.test.ts 同一个 seam):

  • 未 pin 的 reset(改动前 SDK 唯一能表达的那种):HTTP 200 {success:true, reset:true},另一位作者的行没了(sys_metadata 0 行);
  • 同一次陈旧 reset 加上 pin:METADATA_CONFLICT / 409,另一位作者的行还在(1 行);
  • 当前版本 pin 仍然能 reset ⇒ 该 pin 拒绝的是陈旧写入,不是所有写入。

两个 twin 分别断言,没有用一个代替另一个。?state=draft 那组同样:窄 reset 只留下 active 那一行,重复丢弃答 reset:false,全量 reset 才连已发布 overlay 一起删 —— 并且用"state 键在全量调用上缺席"作为该缺席断言的阳性对照。

消融 7 红 / 13 绿,恢复后 20/20,磁盘证明(标记 0→1、blob 哈希位移)先于读判定,恢复以 blob 哈希相等 + git diff HEAD 为空证明。⭐ 并且消融在两个配置文件落地之后出货配置上重跑过一遍,得到同样的 7/13 —— 这一步很多人会省。

声明的 NOT MEASURED,接受

check:dual-build-cjs-loadscheck:type-check-debt --re-measure 在未构建完整 workspace 时拒绝执行并明说 this is NOT a pass;check-half-states.mjs exit 3。三条都报为未测量而非绿。

CI 全绿后我撤草稿并 arm,并在合并队列里看到它出现才算数 —— 本轮 #12946 绿着却两次没入队,arm 调用自己的成功消息不是证据。

两条 out-of-scope 已由 dev 自行立卡(#13023#13024),去重走的是定向 MCP 检索。其中 #13023 值得单独看一眼:meta.deleteItem 声明的返回类型 {type,name,deleted} 这道门从来没答过(真实 body 是 {success, reset?, message?, seq?}),所以任何按 deleted 分支的调用方每次都读到 undefined —— 是本卡测量过程中穿真门测出来的。


Generated by Claude Code

@os-litant
os-litant marked this pull request as ready for review August 29, 2026 00:24
@os-litant
os-litant enabled auto-merge August 29, 2026 00:24
@os-litant
os-litant added this pull request to the merge queueAug 29, 2026
Merged via the queue into main with commit cf71d73Aug 29, 2026
38 checks passed
@os-litant
os-litant deleted the claude/issue-12181-delete-item-carriers branch August 29, 2026 00:37
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

meta.deleteItem sends none of the three carriers the REST reset door reads — no If-Match, no ?state=draft, no ?dropStorage, on either declaration

2 participants

@os-litant@claude