Repository navigation
Description
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
os-litant commentedon Aug 29, 2026
🎯 CLAIMED —
domain:cli执行席(#6024)session_01UjujZN219uFzBhSYfMykCd· 身份os-litantclaude/issue-13023-deleteitem-return-typepm:queue→pm:dispatched;priority:p1/domain:cli保持串行门(裁定 ①)
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全部已合并/关闭)。⇒ 串行释放,本卡可派。卡片正文写的行号是
87042b5de时的读数,已失效。在origin/main(adf70f763)实测:ObjectStackClient.meta.deleteItem:1143packages/client/src/index.ts:1154ScopedEnvironmentClient.meta孪生体:5896packages/client/src/index.ts:6006⛔ 不许按卡片行号跳;每一处都按符号重新定位。
⭐ 卡片正文有一处事实错误,按实测覆盖
卡片把导出类型写成
{ success, reset?, message?, seq?, projectionApplied? }。seq和projectionApplied都不在这个 schema 里。origin/main的packages/spec/src/api/protocol.zod.ts:1197:projectionApplied确实存在于该文件,但在:631/:829的另外两个 schema(data 门的 create/update 响应),不是这一个。至于seq—— 卡片自己的驱动实测已经给出答案:它是message字符串里的[seq=…]文本,从来不是一个键。⇒ 实测的线上形状与导出的 schema 逐键一致。⇒ 正因如此,⛔ 不许手写成员列表。两处都写成绑定导出类型:
手抄一份成员列表正是本卡要修的那个 bug 的复发形态(一份与线上分歧的本地声明),而卡片正文已经示范了它怎么出错。
裁定(转达维护者 2026-08-29 batch #5「同意」,原样约束)
采纳 option 甲:两个
deleteItem孪生体都把返回类型重新声明为 spec 已导出的DeleteMetaItemResponse,在同一个 PR 里(#11713 孪生分歧陷阱)。无人居住的{ type, name, deleted }声明移除。⛔ option 乙(改线上以迎合幻影声明)已被否决 —— 现实即契约。裁决另定的动作:
.deleted的清点结果写进 PR(该键今天在每一次调用上都读到undefined,⇒ 不可能存在依赖它工作的代码);as any强转的测试读法改成有类型的读法;reset,不要读deleted。⛔ 执行约束(硬)
packages/metadata-protocol/src/protocol.ts的deleteMetaItem返回、DeleteMetaItemResponseSchema本身)—— 本卡只动packages/client的声明;reset现在可被有类型地读到(true/false两支都有断言);②.deleted不再可读(类型层面,return-type-precision.test.ts是既有落点);it.fails/skip/as any绕过新类型;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
os-litant commentedon Aug 29, 2026
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