Uh oh!
There was an error while loading. Please reload this page.
fix(client): declare the response the meta reset door actually sends on both deleteItem twins - #13148
fix(client): declare the response the meta reset door actually sends on both deleteItem twins#13148os-litant wants to merge 1 commit into
deleteItem twins#13148Conversation
Both `deleteItem` declarations — the unscoped `ObjectStackClient.meta` and the
environment-scoped `ScopedEnvironmentClient.meta` twin — declared
`Promise<{ type: string; name: string; deleted: boolean }>`. That shape is not
merely imprecise, it is uninhabited: `DELETE /meta/:type/:name` ends in
`res.json(result)` with `deleteMetaItem`'s return, and not one of that method's
four return branches carries `type`, `name` or `deleted`. So a caller who
branched on the documented `deleted` flag read `undefined` — falsy — on every
reset, including the ones that really removed an overlay row.
Both twins now BIND `DeleteMetaItemResponse`, the type `@objectstack/spec`
already exported, rather than transcribing its members: a hand-written member
list is the same defect one layer up. The wire is untouched.
`os meta delete` read the phantom key too and now reports `result.reset`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd📓 Docs Drift CheckThis PR changes 3 package(s): 19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 3 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 133 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 a4aeb4457cb01b3a84eff1892742cc64dec4786c && git checkout a4aeb4457cb01b3a84eff1892742cc64dec4786c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 56470d86bf25269b90fad0123cbee6282bc08be7 e7b5ee7f3645e5b761c7ce2650fb220bd5d94559 && git checkout -B drift-repro 56470d86bf25269b90fad0123cbee6282bc08be7 && git merge --no-ff e7b5ee7f3645e5b761c7ce2650fb220bd5d94559
node scripts/docs-audit/affected-docs.mjs --json 56470d86bf25269b90fad0123cbee6282bc08be7
|
os-litant
commented
Aug 29, 2026
✅ PM 复核 —— ACCEPT。⭐ 并且我的派发令有两处事实错误,dev 都测出来了,我照单认下
⛔ 先认错:我在派发令里写错了两件事(a) 我写「实测的线上形状与导出的 schema 逐键一致」—— 这是假的。
return{success: true,reset: true,seq: result.seq,
...(deleteProjection ? {projectionApplied: deleteProjection} : {}),⇒ 线上确实送 ⇒ 绑定 spec 类型因此是一次相对线上的真实收窄,⛔ 不是我说的「恒等」。裁定的动作不变(spec 即契约),但这一点必须按收窄记录,而不是按恒等 —— dev 的措辞是对的,我采纳。 (b) 我把 实测 ⭐ 这个更正把一次巧合变成了一条同族不对称,比我原来的读法重要得多:同一道门的三个写动词,跑同一个 ADR-0094 projector、同一次 history append、三个都在线上发 ⇒ dev 的越界 finding 因此成立且值得立卡。 ⭐ 顺带记一条:卡片正文把 ✅ 核心判据:幻影三元组已消失,且是带阳性对照的零
两个孪生体都绑定了导出类型( 三处超出派发字面的动作,逐条裁1. ⭐ 而这一处顺带暴露了一个真实的用户可见缺陷,注释里写明了: 2. ADR-0087 语义迁移条目(
3. 重生成 报告里另外三条我核过、且值得记的仪器纪律
处置ACCEPT。 保持 Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Closes#13023
Both
deleteItemtwins inpackages/client/src/index.tsdeclaredPromise<{ type: string; name: string; deleted: boolean }>. The declaration is notmerely imprecise — it is uninhabited. Both now declare
DeleteMetaItemResponse,the type
@objectstack/specalready exports. The wire is untouched.Ruled by the maintainer on 2026-08-29 (batch #5), option 甲, verbatim 「同意」. Option 乙
— changing the wire to satisfy the phantom declaration — was refused: reality is the
contract.
The declaration was uninhabited on every branch, measured
DELETE /meta/:type/:nameends inres.json(result)(
packages/rest/src/rest-server.ts:5942) withdeleteMetaItem's return. That method hasfour return branches, and I read all four rather than sampling one:
{ success, reset: false, message }{ success, reset: true, seq, projectionApplied?, message }{ success, reset: false, message }{ success, reset: true, message }No branch carries
type,nameordeleted. And nothing strips anything on the wayback:
unwrapResponseunwraps only a body with both a booleansuccessand adatakey, and this body has nodata— so the caller receives the door's whole bodyand the annotation describes it exactly.
So
r.deletedcompiled and readundefinedon every reset, including the ones thatreally removed a row. The truthful flag is
reset, and itsfalsearm — "no overlay rowexisted, already at artifact default" — is precisely the case a caller most wants to tell
apart.
1. The card body's
{ success, reset?, message?, seq?, projectionApplied? }wasattributed to the wrong source — but the keys are real. The card wrote that as
DeleteMetaItemResponseSchema's shape. It is not: that schema(
packages/spec/src/api/protocol.zod.ts:1197) declares exactly{ success, reset?, message? }. The card was quoting the implementation's declaredreturn in
@objectstack/metadata-protocol, which does nameseqandprojectionApplied.2. Which means the wire and the schema do NOT agree key for key. Branch B really does
put
seqon the wire, andprojectionAppliedtoo when a mutation projector isregistered.
DeleteMetaItemResponseSchemadeclares neither. The ruling binds the spectype, so both stay unreadable from the SDK — a deliberate, stated narrowing relative to
the wire, not an oversight. That producer-side declared-vs-sent gap is a finding of its
own — recorded at the end of this description, and NOT filed as an issue, for the reason
stated there; it is pinned here as a green-in-both-states guard so a later sweep cannot
"complete" the annotation by hand. (
seqis also rendered as[seq=…]text inside
message, which is what the card's driven measurement saw — but it is atop-level key as well.)
Neither correction changes the ruled action, and the card's core premise survives intact:
the
{ type, name, deleted }shape is uninhabited on all four branches.Why the type is BOUND, not transcribed
⛔ No member list is hand-written here. A local declaration that drifts from the wire is
the exact defect this PR removes, and the card body just demonstrated how it goes wrong.
Both twins name
DeleteMetaItemResponse; consumers reach it asimport type { DeleteMetaItemResponse } from '@objectstack/spec/api'—@objectstack/specis already a runtime dependency of
@objectstack/client, so no new export is added tothis package. The published surface of
@objectstack/clientis unchanged.Both twins land in one PR (the #11713 twin-divergence trap). The declarations were
TEXTUALLY IDENTICAL, so a global count cannot tell "both fixed" from "half the fix
landed" — every pin below is therefore made twice, once per client.
Census: external code branching on
.deletedResult: no working code exists on the key, and exactly one in-repo consumer read it.
Method.
git grep -nE '\.deleted\b'across every tracked file of this repo(
node_modules/distexcluded), plus the same sweep over the first-party siblingobjectstack-ai/objectuiat190fbd0, then each hit classified by hand. Call sites wereenumerated separately with
git grep -n 'meta\.deleteItem'and each one read.Positive control (an absence claim needs one). The same pattern does find
.deletedproperty reads where they exist — it returned
r.deletedinpackages/client/src/data-delete-result-shape.test.ts,result.deletedinpackages/cli/src/commands/data/delete.ts,r.deleted ?? 0in objectui'sMarketplacePackagePage.tsx, andreceipt.deletedin objectui'sdeleteView.homes.test.ts.A second, independent control: the one true positive below was found by grep and then
confirmed by the compiler, which named the same line once the type narrowed. The
instrument is not blind.
In this repo — one hit, fixed here.
packages/cli/src/commands/meta/delete.tsbuiltits
--format json/--format yamlpayload withdeleted: result.deleted. Measuredwith the command's real formatter:
JSON.stringifydrops undefined values, and so doesyaml.stringify—— so the
deletedkey this command has always declared never appeared in a singlerun, in either machine format. No script can be parsing it. It now carries
result.reset. The key name staysdeleteddeliberately: it is the CLI's output key,not the protocol's, and the payload's top-level
successalready means somethingdifferent (the CLI envelope's "the command completed"). That is the treatment #5638 gave
the sibling
os data delete, verbatim.In objectui — no break, and the pinned Console gate is safe. Two facts, both measured
at
190fbd0:data-objectstackno longer routes throughclient.meta.deleteItemat all (itmoved to its own
MetadataClient.reset); the only mention left is prose. Its one.deletedread,readViewDeleteReceipt, takesresult: unknownand casts toRecord<string, any>— it never passes through the client's declared type, so nothingstops compiling. Its ladder is already
r?.deleted ?? r?.reset ?? true, and its owncomment names this very divergence: "the framework sends
reset, the SDK's typedmetadata shape names
deleted". After this PR both spell itreset; that PR's firstrung becomes dead code, which is an objectui follow-up, not a break.
apps/console/.../AppManagementPage.tsx:274, awaits and discardsthe result through a locally declared
deleteItem: (type, name) => Promise<unknown>—structurally satisfied by the narrower return.
No export is removed or renamed by this PR, so AGENTS.md §"a removal that breaks the
pinned sibling checkout" does not engage.
Tests — both directions, twice each
packages/client/src/return-type-precision.test.ts(the existing landing site), newreturnTypePrecisionPins13023():toEqualTypeOf<DeleteMetaItemResponse>()on both twins, andresettyped asboolean | undefinedon both (the schema's optionality kept: a"narrowing" to a required
booleanwould be a fresh false declaration).@ts-expect-errorpins that.deleted,.typeand.nameare nolonger readable, on both twins. These are the point of the card: each was unused
(TS2578) while the phantom declaration stood, so the defect is now stated as a compile
error at the call site where a consumer would have written it.
evidence:
DeleteDataResponse(the DATA door's body, one import away — the near-miss afuture sweep will reach for) is refused for this door, and
seq/projectionAppliedare pinned as declared-by-the-producer-but-not-by-the-contract.
packages/client/src/meta-delete-item-carriers.test.ts— the behavioural half,against a real
RestServer+ realObjectStackProtocolImplementation+ realsys_metadatatables:const r: any = await …deleteItem(…)casts are nowconst r: DeleteMetaItemResponse— typed reads, per the ruling. They wereanyprecisely because the declared return named none of the fields the door sends.
door:
reset === truewhere a row was deleted,reset === falsewhere none existed(the repeated draft discard, and the "nothing to delete" arm).
deleted,typeandnameare absent, withsuccess/resetasserted present on the same instrument as the positive control.⛔ No
it.fails, noskip, and noas anyleft anywhere to dodge the new type.Reverse verification
Direction predicted before running: red. Revert only
packages/client/src/index.tstothe branch point, keep every pin.
declarations, and a
git hash-objectdiffering from the HEAD blob.files import the subject as
./index(source-relative), so tsc reads the mutatedsrc/index.tsdirectly — nothing on this path resolves through the package'sexports→
dist.pnpm --filter @objectstack/client typecheckexits 1, andcheck:test-typecheckprints its own verdict — "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.
the HEAD blob and
git diff HEADis empty. Restore wasgit checkout HEAD -- <abs path>(never a bare
git checkout --, which would have read the index the mutation wrote).Changeset and ADR-0087
minoron@objectstack/clientand@objectstack/cli— breaking changes ship asminorduring the launch window, and the body carries the FROM → TO (
r.deleted→r.reset)with the behavioural warning that every
if (r.deleted)has been false since it waswritten.
Registered in the ADR-0087 ledger as the semantic entry
client-meta-reset-result-reset— the exact parallel ofclient-delete-result-success(#5638) one door over. It is the only honest disposition available: the body carries a
migration prescription (so
no-migration-prescriptionis refused by the gate), bothpackages are published (so
unpublishedis refused), and the sibling entry covers theDATA door, not this one. The gate agrees:
spec-changes.jsonanddocs/protocol-upgrade-guide.mdwere regenerated and arebyte-unchanged — correctly so, and checked rather than assumed: no step-18 semantic
entry projects into either artifact yet (verified against two existing step-18 siblings as
the control).
check:spec-changesandcheck:upgrade-guideboth report "up to date".⛔
content/docs/releases/is untouched.Verification — all at
e7b5ee7f3, the final commitpnpm --filter '@objectstack/client^...' buildos-verify-lock: VERDICT command-exit 0pnpm --filter @objectstack/client buildvitest runon the 3 affected filesTest Files 3 passed (3) · Tests 28 passed (28)pnpm --filter @objectstack/client typechecktsc --noEmit+check:test-typecheck)pnpm --filter '@objectstack/cli^...' build→pnpm --filter @objectstack/cli typecheckerror TSlinescheck:migration-registry✓ src/migrations/registry.ts is current (136 semantic, 69 retired-key, 71 retired-def)check:spec-changesspec-changes.json is up to date.check:upgrade-guideprotocol-upgrade-guide.md is up to date.check-adr-0087-registration✓ … 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.check-changeset-no-major✓ This diff introduces no 'major' bump.check:nul-bytesOK (scanned 7289 text file(s) … no raw ASCII control bytes)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-authoringThe families were re-derived from the real change set with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(7 paths vs mergebase
4d94823a3) rather than recalled.@objectstack/clitypecheck is worth stating precisely, because its first readingwas a false red: run before its dependency closure was built it reported
Exit status 2with 44 diagnostics, every one of them
TS2307 Cannot find module '@objectstack/…'andnone in a file this PR touches. That is the unbuilt-closure trap, which reads exactly like
"your change broke an import". With
pnpm --filter '@objectstack/cli^...' buildrunfirst, the same command exits 0 with zero diagnostics.
Lint is a declared narrowing, not a full-repo run, with the three things that make a
narrowing a measurement rather than an omission:
ignore verdict is ESLint's;
--format json: 7 entries, 6 linted, 1 ignored (the.changeset/*.md), 0 errors, exit 0;eslint.config.mjs, whichnever enables type-aware linting for any file (no
parserOptions.project, no typed@typescript-eslintrules) — stated in that file at line 327 with its own measuredpositive control. This PR changes no ESLint config, so no untouched file's verdict can
move.
CI runs the full farm regardless; this is the cheap half, not a substitute for it.
Out-of-scope finding, NOT addressed here
The metadata door's three write verbs all run the same ADR-0094 projector and the same
history append, and all three emit
seq+projectionApplied. Two of them declarethose keys —
SaveMetaItemResponseSchema(protocol.zod.ts:611) andPublishMetaItemResponseSchema(:774) — and the third,DeleteMetaItemResponseSchema(
:1197), does not, whiledeleteMetaItem's branch B sends both. That is aproducer-side declared-vs-sent gap in the #11679 family, on the spec side rather than the
SDK side, and it is out of scope here: this PR was ruled to change only the SDK
declaration, and correcting the schema is an accept-set change of its own.
It could not be filed from this seat: repo-scoped REST is refused for this session class
(
GitHub access is not enabled for this session), and the one targeted MCP search thefallback allows returned
API rate limit already exceeded, so the mandatorysearch-before-filing step could not be completed. It is handed to the PM with the
measurement above rather than filed unverified or dropped.
:631and:829are not the datadoor's create/update responses — they are the metadata door's own save and publish
responses. That is what makes the gap a same-family asymmetry rather than a coincidence.
Generated by Claude Code