Skip to content

meta.deleteItem declares a return type — { type, name, deleted } — that the reset door has never answered #13023

Closed

Description

@os-litant

Measured while implementing #12181 (the deleteItem carrier half), on origin/main at 87042b5de. Reported rather than folded in: #12181's ruling is scoped to the request-side carriers, and correcting a published return type is an accept-set change of its own.

What was measured

Both deleteItem declarations in packages/client/src/index.ts (the unscoped ObjectStackClient.meta and the environment-scoped ScopedEnvironmentClient.meta twin) declare:

Promise<{ type: string; name: string; deleted: boolean }>

The door answers something else entirely. DELETE /meta/:type/:name ends with res.json(result) where result is deleteMetaItem's return, and both the implementation (packages/metadata-protocol/src/protocol.ts) and the contract (DeleteMetaItemResponseSchema, packages/spec/src/api/protocol.zod.ts) declare:

{ success: boolean; reset?: boolean; message?: string; seq?: number; projectionApplied?: … }

There is no type, no name, and no deleted on any branch of the response. Driven end to end against a real RestServer + real ObjectStackProtocolImplementation + real sys_metadata tables (the harness added in #12181's PR), a successful reset answers { success: true, reset: true, message: 'Customization overlay deleted — view/… reset to artifact default. [seq=…]' }.

Why it is worth a card

The declared shape is not merely imprecise, it is uninhabited: a first-party caller who branches on the documented deleted flag reads undefined — falsy — on every reset, including the ones that really deleted a row. The truthful signal is reset, and the "nothing was there" case (success: true, reset: false) is exactly the one a caller most wants to distinguish. The types compile, so nothing surfaces this; the SDK's own tests had to cast through any to read the real fields.

DeleteMetaItemResponse is already exported from @objectstack/spec, so this is a declaration catch-up (the #11679 shape, one layer up) rather than a new contract.

Scope note

Both declarations carry the same wrong shape, so a fix must land on both — the same twin-divergence trap #12181 and #11713 record. Narrowing a published return type is a breaking change to anyone who wrote .deleted; the ruling would need to decide between correcting the type outright and declaring the response the spec already declares.

Related: #12181 (the request-side carriers on this same method), #11679 (the same declared-vs-sent gap on DeleteMetaItemRequest), and packages/client/src/return-type-precision.test.ts.

Activity

  1. added
    priority:p1High: required for production / M2
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they land
    on Aug 28, 2026
  2. added theissue type on Aug 28, 2026
  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 29, 2026
  4. self-assigned this
    on Aug 29, 2026
  5. os-litant commented on Aug 29, 2026

    @os-litant
    ContributorAuthor

    🎯 CLAIMED — domain:cli 执行席(#6024)

    • 会话:session_01UjujZN219uFzBhSYfMykCd · 身份 os-litant
    • 分支:claude/issue-13023-deleteitem-return-type
    • 标签:pm:queue → pm:dispatched;priority:p1 / domain:cli 保持
    • Clause-②:no(把一个已导出的类型接上去,不新增契约面)

    串行门(裁定 ①)

    packages/client/src/index.ts 是同文件 HARD SERIAL。释放它的是 MERGE,不是 arm:#13082(fix(client): bind the five in-repo return res.json() methods…)已于 b15d260d1 落地 origin/main。已按分支实测复核,当前没有任何开放 PR 触碰该文件(唯四命中该文件的分支 issue-11926 / issue-12034 / issue-12104 / issue-12181 全部已合并/关闭)。⇒ 串行释放,本卡可派。

    ⚠️ R49-1:坐标已被释放的那次合并移动

    卡片正文写的行号是 87042b5de 时的读数,已失效。在 origin/main(adf70f763)实测:

    卡片所写 当前 main 实测
    非作用域 ObjectStackClient.meta.deleteItem :1143 packages/client/src/index.ts:1154
    作用域 ScopedEnvironmentClient.meta 孪生体 :5896 packages/client/src/index.ts:6006

    ⛔ 不许按卡片行号跳;每一处都按符号重新定位。

    ⭐ 卡片正文有一处事实错误,按实测覆盖

    卡片把导出类型写成 { success, reset?, message?, seq?, projectionApplied? }。seq 和 projectionApplied 都不在这个 schema 里。origin/main 的 packages/spec/src/api/protocol.zod.ts:1197:

    export const DeleteMetaItemResponseSchema = lazySchema(() => z.object({
      success: z.boolean(),
      reset: z.boolean().optional(),
      message: z.string().optional(),
    }));

    projectionApplied 确实存在于该文件,但在 :631 / :829 的另外两个 schema(data 门的 create/update 响应),不是这一个。至于 seq —— 卡片自己的驱动实测已经给出答案:它是 message 字符串里的 [seq=…] 文本,从来不是一个键。⇒ 实测的线上形状与导出的 schema 逐键一致。

    ⇒ 正因如此,⛔ 不许手写成员列表。两处都写成绑定导出类型:

    Promise<DeleteMetaItemResponse>

    手抄一份成员列表正是本卡要修的那个 bug 的复发形态(一份与线上分歧的本地声明),而卡片正文已经示范了它怎么出错。

    裁定(转达维护者 2026-08-29 batch #5「同意」,原样约束)

    采纳 option 甲:两个 deleteItem 孪生体都把返回类型重新声明为 spec 已导出的 DeleteMetaItemResponse,在同一个 PR 里(#11713 孪生分歧陷阱)。无人居住的 { type, name, deleted } 声明移除。⛔ option 乙(改线上以迎合幻影声明)已被否决 —— 现实即契约。

    裁决另定的动作:

    1. 对外部代码分支于 .deleted 的清点结果写进 PR(该键今天在每一次调用上都读到 undefined,⇒ 不可能存在依赖它工作的代码);
    2. SDK 自己那些 as any 强转的测试读法改成有类型的读法;
    3. changeset 按策略定级 —— 这是一次已发布返回类型的收窄,changeset 正文须写明迁移处方:读 reset,不要读 deleted。

    ⛔ 执行约束(硬)

    1. 两处一起改,⛔ 不许只改一个孪生体;
    2. ⛔ 不许改线上(packages/metadata-protocol/src/protocol.ts 的 deleteMetaItem 返回、DeleteMetaItemResponseSchema 本身)—— 本卡只动 packages/client 的声明;
    3. 两个方向都要 pin:① reset 现在可被有类型地读到(true / false 两支都有断言);② .deleted 不再可读(类型层面,return-type-precision.test.ts 是既有落点);
    4. ⛔ 不许 it.fails / skip / as any 绕过新类型;
    5. ⛔ 不许碰 content/docs/releases/。

    环境纪律

    ⛔ 必须在专用 worktree 里开工(git worktree add ../objectstack-13023 -b claude/issue-13023-deleteitem-return-type origin/main),⛔ 全程不许 git stash(stash 栈在所有 worktree 间共享)。


    Generated by Claude Code

  6. os-litant commented on Aug 29, 2026

    @os-litant
    ContributorAuthor

    os-dev-report

    {
      "issue": 13023,
      "status": "done",
      "branch": "claude/issue-13023-deleteitem-return-type",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/13148",
      "premise_still_valid": true,
      "summary": "Both deleteItem twins (packages/client/src/index.ts:1154 unscoped, :6006 scoped — both relocated by symbol, matching the PM's re-measurement even though origin/main had advanced to 4d94823a3) now declare Promise<DeleteMetaItemResponse>, BOUND to the spec's exported type with no member list transcribed. The wire is untouched. Premise verified, not assumed: I read all FOUR return branches of deleteMetaItem and none carries type, name or deleted, and unwrapResponse passes the body through (it unwraps only a body with both a boolean success AND a data key; this one has no data). THREE THINGS EXCEEDED THE LITERAL DISPATCH AND NEED PM REVIEW. (1) packages/cli/src/commands/meta/delete.ts was a forced consumer fix, not optional: it read result.deleted, so the narrowing makes it a compile error. Fixed exactly as the landed #5638 sibling fixed `os data delete` — the CLI keeps its own output key name and fills it with the protocol's truthful value (deleted: result.reset). (2) The changeset declares BREAKING, which makes an ADR-0087 disposition mandatory; the only honest marker was `registered`, so I authored the semantic entry client-meta-reset-result-reset (packages/spec/src/migrations/entries/semantic/) and ran gen:migration-registry. That touches packages/spec — not a governed surface, but beyond the card's literal wording. (3) I also regenerated spec-changes.json and the upgrade guide; both came back byte-unchanged, which I verified is correct rather than a no-op (no step-18 semantic entry projects into either artifact yet, checked against two existing step-18 siblings as the control).",
      "tests": "All at final commit e7b5ee7f3, tree clean. Heavy steps via scripts/pm/os-verify-lock.sh. BUILD: `pnpm --filter '@objectstack/client^...' build` → os-verify-lock VERDICT command-exit 0; `pnpm --filter @objectstack/client build` exit 0. VITEST (targeted, --maxWorkers=2, never args after a bare --): meta-delete-item-carriers.test.ts + return-type-precision.test.ts + data-delete-result-shape.test.ts → 'Test Files 3 passed (3) / Tests 28 passed (28)'. TYPECHECK: `pnpm --filter @objectstack/client typecheck` (tsc --noEmit && check:test-typecheck) exit 0. CLI: first reading was a FALSE RED — Exit status 2 with 44 diagnostics, every one TS2307 'Cannot find module @objectstack/...', none in a file this PR touches: the unbuilt-closure trap. After `pnpm --filter '@objectstack/cli^...' build` (exit 0) the same typecheck exits 0 with ZERO error TS lines and no diagnostic naming commands/meta/delete.ts. REVERSE VERIFICATION (direction predicted before running: RED): reverted only packages/client/src/index.ts to the branch point, kept every pin. Mutation confirmed ON DISK before any reading — 2 phantom declarations back, 0 bound declarations, and git hash-object differing from the HEAD blob — never an editor exit code. NO REBUILD LEG IS OWED and the reason is structural, not an omission: both pin files import the subject as './index' (source-relative), so tsc reads the mutated src/index.ts directly; nothing on this path resolves through the package exports to dist. Result: typecheck exit 1, and check:test-typecheck printed its own verdict lines — 'src/meta-delete-item-carriers.test.ts: 7 type error(s)' and 'src/return-type-precision.test.ts: 10 type error(s)', in files the debt ledger does not cover. Restore proven by observed STATE not exit code: byte-identical to the HEAD blob and `git diff HEAD` empty; restore was HEAD-pinned (git checkout HEAD -- absolute-path), never a bare checkout, and the script carried a trap with absolute paths. CAUTION ON ONE NUMBER: my grep for TS2578 in the ablation log returned 2, but both hits are the gate's own ADVICE PROSE, not diagnostics — the gate's printed error counts above are the real evidence. GATES (families re-derived from the real change set with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack`, 7 paths vs merge base 4d94823a3, not recalled; each verdict quoted from the gate's own output, exit codes captured before any pipe): check:migration-registry 'registry.ts is current (136 semantic, 69 retired-key, 71 retired-def)'; check:spec-changes 'spec-changes.json is up to date.'; check:upgrade-guide 'protocol-upgrade-guide.md is up to date.'; check-adr-0087-registration '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition ... registered client-meta-reset-result-reset (new here)'; check-changeset-no-major 'This diff introduces no major bump.'; check:nul-bytes 'OK (scanned 7289 text file(s) ... no raw ASCII control bytes)'; plus exit 0 each on check:authorable-surface, check:cross-package-test-inputs, check:test-source-alias, check:type-source-resolution, check:published-files, check:changeset-gate-self-tests, check:objectui-changeset, check:pm-half-states, check:doc-authoring. LINT IS A DECLARED NARROWING with all three evidence items: population from ESLint's own config (the per-file ignore verdict is ESLint's); count from --format json = 7 entries, 6 linted, 1 ignored, 0 errors, exit 0; and config invariance — this repo's single eslint.config.mjs never enables type-aware linting for any file (stated at line 327 with its own measured positive control), and this PR changes no ESLint config, so no untouched file's verdict can move. CI runs the full farm regardless.",
      "open_questions": [],
      "out_of_scope_findings": [
        "NOT FILED — blocked, handed to PM with the measurement. The metadata door's three write verbs all run the same ADR-0094 projector and all three emit seq + projectionApplied, but only two DECLARE them: SaveMetaItemResponseSchema (protocol.zod.ts:611) and PublishMetaItemResponseSchema (:774). DeleteMetaItemResponseSchema (:1197) declares neither, while deleteMetaItem's repository-delete branch sends both. A producer-side declared-vs-sent gap in the #11679 family, on the spec side. Filing was impossible from this seat: repo-scoped REST returns 'GitHub access is not enabled for this session' (the documented os-dev session-gate class, rest-channel.md 2026-08-25), and the single targeted MCP search that fallback allows returned 'API rate limit already exceeded', so the mandatory search-before-filing step could not be completed. I did not file unverified. It is pinned in the PR as a green-in-both-states guard so a later sweep cannot 'complete' the annotation by hand."
      ],
      "contradicted_by_measurement": [
        "THE CARD: its '{ success, reset?, message?, seq?, projectionApplied? }' was attributed to DeleteMetaItemResponseSchema. That schema declares only { success, reset?, message? }. The card was quoting the IMPLEMENTATION's declared return in @objectstack/metadata-protocol, which does name both extra keys. The PM's correction of the schema is right.",
        "THE PM's DISPATCH, on two points. (a) 'the measured wire shape and the exported schema agree key for key' is FALSE: the repository-delete branch really does put seq on the wire, and projectionApplied too when a mutation projector is registered, and the schema declares neither. Binding the spec type is therefore a real narrowing relative to the wire — still correct under the ruling, since the spec is the contract, but it should be recorded as such rather than as an identity. (b) 'projectionApplied ... exists at :631 and :829, in two OTHER schemas (the data door's create/update responses)' misidentifies them: :631 is SaveMetaItemResponseSchema and :829 is PublishMetaItemResponseSchema — the METADATA door's own save and publish responses. That is what turns the gap into a same-family asymmetry rather than a coincidence, and it is why the finding above is worth a card.",
        "NEITHER correction changes the ruled action, and the card's core premise stands: { type, name, deleted } is uninhabited on all four return branches."
      ]
    }

    Generated by Claude Code


    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

Labels

domain:cliRetired 2026-10-22, alias of domain:server (cli surface); in-flight cards keep it until they landpm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p1High: required for production / M2

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions