Skip to content

meta.saveItem's own docstring tells the caller to echo version back as an If-Match header — the SDK offers no way to send one, on either declaration #11713

Description

@os-zhuang

Measured while implementing #11391. Same defect shape as that card — the SDK prescribing something its own surface cannot do — one parameter over, so filed separately rather than folded: #11391's ruling enumerated exactly three query parameters (force, packageId, mode) and this is a fourth carrier, a request header, which is a contract decision of its own.

What was measured

The docstring directly above meta.saveItem in packages/client/src/index.ts reads:

The resolved version is the ADR-0008 optimistic-concurrency token: echo it back as the If-Match request header on the next write to the same item and a concurrent edit is reported as 409 metadata_conflict instead of silently overwriting.

Neither saveItem declaration accepts a header, an ifMatch option, or anything else that becomes one:

$ grep -n "If-Match\|ifMatch" packages/client/src/index.ts
696:     * echo it back as the `If-Match` request header on the next write to the
939:     * `If-Match` on the next write to the item. It is nameable here only since
1295:     * `version` is the ADR-0008 OCC token (echo as `If-Match`), and the
4689:        opts?: { ifMatch?: string },
4698:        if (opts?.ifMatch) headers['If-Match'] = String(opts.ifMatch);
4787:        opts?: { ifMatch?: string },
4792:        if (opts?.ifMatch) headers['If-Match'] = String(opts.ifMatch);

Lines 696 / 939 / 1295 are the three places the SDK tells a caller to use If-Match. Lines 4689–4792 are two other methods that actually implement it — so this is an absent option on saveItem, not a client that cannot send the header.

The server side is live: the REST PUT handler reads it and threads it as parentVersion:

const ifMatchHeader = req.headers?.['if-match'] ?? req.headers?.['If-Match'];
const parentVersion = typeof ifMatchHeader === 'string'
    ? ifMatchHeader.replace(/^"|"$/g, '')
    : undefined;

So the ADR-0008 optimistic-concurrency protection the docstring describes is reachable over raw HTTP and unreachable through @objectstack/client — a first-party SDK caller who follows the instruction has nowhere to put the token, and their concurrent edit silently overwrites instead of answering 409 metadata_conflict. The sibling first-party client MetadataClient in @object-ui/data-objectstack carries ifMatch in its own save options for exactly this reason.

Why it was not folded into #11391

That card's ruling names the three query parameters and the options bag that carries them. If-Match is a header rather than a query parameter, and adding it widens the published SDK surface a fourth time beyond what was ruled — so it is reported rather than taken. The bag landed by #11391 (SaveMetaItemOptions) is the natural place for it if that is ruled in.

Where it would land

packages/client/src/index.ts — both saveItem declarations, which must stay in step — plus pins in packages/client/src/client.test.ts on the headers the methods build (the existing #11391 pins measure the URL only).

Related: #11391 (the query-string half), and ADR-0008 for the OCC contract itself.

Measured on origin/main at 4ceae8ab0.


Generated by Claude Code


Generated by Claude Code

Activity

  1. os-zhuang commented on Aug 24, 2026

    @os-zhuang
    ContributorAuthor

    分诊(session session_01LrqnyLhY3kYBBRird4peBT,2026-08-24):入决策箱(needs-user-decision + domain:cli,type Feature)。#11391 的裁决逐项列举了恰好三个 query 参数并明令「report rather than silently widen」—— 本卡是第四个载体(请求头),被刻意围栏在外,继承不适用,须单独裁。四棱卡面块如下。

    os-decision-facets

    一句话问题:SDK 自己的文档告诉调用者「把 version 用 If-Match 头带回来,就能防止两个人同时改一条元数据时互相覆盖」,但 saveItem 根本没有地方放这个头 —— 照文档做的人,并发编辑仍然静默互相覆盖,而不是收到 409 冲突提示。

    选项 × 真实代价:

    四棱:
    ① 实际业务拉动:并发覆盖是真实数据丢失场景;服务端 If-Match→parentVersion 已上线,同门客户端已实测需要它 —— 拉动是实的,不是投机面。
    ② 项目长远合理性:ADR-0008 乐观并发是平台既定契约;第一方 SDK 是该契约的正门,正门缺口比旁门(raw HTTP)长期更贵。A 与 #11391 的修复形状同族(同一个 options 袋),不是新形态。
    ③ 防 AI 犯错:今天的形态是最坏的 —— 文档声明了、类型系统不拒绝、运行时静默覆盖;AI 照文档生成的代码看起来对、实际丢数据。A 让声明=强制;B 至少消灭假声明。
    ④ 创业阶段不扩散:A 是一个可选参数进既有袋,边际极小;真正的扩散风险(query 参数枚举)已被 #11391 围栏,本项不破栏,只是栏外单独一票。

    推荐:A(ifMatch 进 SaveMetaItemOptions,双声明同步,headers pin 补上);回退 B(同 PR 删三处指引,绝不留 C)。置信缺口:未测有多少外部消费者已在用 raw HTTP 绕行(A 不破坏他们);未测 MetadataClient 与本 SDK 的 ifMatch 语义是否逐字一致(实现时对齐)。实施 PR 属 Clause-②(扩大公开面)⇒ 契约复审档位。

    低摩擦裁决格式:回「A」/「B」即可;「A,但 X」按 A 落地并记 X。


    Generated by Claude Code

  2. os-support-ai commented on Aug 25, 2026

    @os-support-ai
    Collaborator

    Maintainer ruling — A: ifMatch joins SaveMetaItemOptions

    Source: maintainer, 2026-08-25, live PM chat (decision-inbox batch 3 review, session session_01KWRU3s15AJz7PGW7a7wdCh), verbatim: 「同意」 — accepting the batch's presented recommendation for this card: A.

    Implementation shape, as presented and accepted:

    Clause-②: yes (widens the published SDK surface) — contract-review tier on the implementation.

    State: needs-user-decision → pm:queue in the same write; domain:cli stands.


    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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions