You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The first-party SDK's meta.saveItem sends no query string at all, so the ?force=true the destructive 409 prescribes is unreachable through @objectstack/client on either REST door #11391
Measured while implementing #11095 (threading ?force on the compound-name PUT /meta/:type/:a/:b). Separate surface, filed rather than folded — it needs a contract decision of its own and #11095's ruling does not reach it.
What was measured
saveMetaItem's Phase 3a-destructive gate raises 409 DESTRUCTIVE_CHANGE and ends the message with — re-submit with ?force=true to proceed. After #11095 that clause is TRUE of both REST PUT doors: each reads ?force off the query string and threads it.
It is still unreachable from the first-party SDK. @objectstack/client declares meta.saveItem twice — the unscoped client and the environment-scoped one — and neither builds a query string at all:
packages/client/src/index.ts:703 — saveItem: async (type, name, item) → PUT ${route}/${type}/${name}, body only.
packages/client/src/index.ts:5151 — the scoped twin → PUT /meta/${type}/${name}, body only.
Measured on origin/main @ 11038529: grep -c force packages/client/src/index.ts is 9, and not one of those hits is on a metadata save — they are environments.delete({ force }) (?force=1), unrelated prose, and the word "enforced". The sibling getItem on the same object does compose a query string (if (options?.packageId) params.set('package', …)), so this is an absent option on one method rather than a client that cannot send query parameters.
So an SDK caller refused with the destructive 409 is told to re-submit with a parameter their client offers no way to set. The remedy is reachable only by dropping to raw fetch.
#11095 was about two ROUTES that did not read a parameter callers could send. This is one CLIENT that cannot send a parameter both routes now read — a different layer, a different fix, and a different decision. It also predates #11095 on the single-segment door: the clause has been unactionable for SDK callers there the whole time, so this is not a regression that card introduced.
It is equally absent on both doors, so it is not a twin divergence either, and #7019's "twin doors must not diverge" argument — the load-bearing one for #11095's compound-name half — says nothing about it.
Why it is a contract question rather than an obvious fix
Adding force to meta.saveItem widens the published SDK surface, which is the same tier of decision #11095 was escalated for. Candidate shapes, none of them free:
A — an options bag: saveItem(type, name, item, { force?: boolean }). Matches getItem's existing options?.packageId shape on the same object, and leaves room for the twin's other two unexposed query parameters (?package, ?mode=draft), which are in exactly the same position. Widest, and the one that makes the whole door's surface reachable.
B — force alone, narrowest, on the argument that only this parameter is named by a refusal the caller is expected to act on. Leaves ?package / ?mode unreachable and invites a second card.
Worth measuring before choosing, and not measured here: whether Studio and the CLI go through meta.saveItem or through raw HTTP for metadata saves. If the real destructive-edit surfaces already bypass the SDK, option C's cost is much lower than it looks, and option A is speculative surface.
Where the repair would land
packages/client/src/index.ts (both saveItem declarations, which must stay in step — they are the same method on two clients), plus whatever pins client.test.ts holds on the URL those methods build.
Related: #11095 (the routes, fixed), #11015 (the face-aware remedy clause), #10886 (the sole-carrier verdict on this message).
Measured while implementing #11095 (threading
?forceon the compound-namePUT /meta/:type/:a/:b). Separate surface, filed rather than folded — it needs a contract decision of its own and #11095's ruling does not reach it.What was measured
saveMetaItem's Phase 3a-destructive gate raises409 DESTRUCTIVE_CHANGEand ends the message with— re-submit with ?force=true to proceed.After #11095 that clause is TRUE of both RESTPUTdoors: each reads?forceoff the query string and threads it.It is still unreachable from the first-party SDK.
@objectstack/clientdeclaresmeta.saveItemtwice — the unscoped client and the environment-scoped one — and neither builds a query string at all:packages/client/src/index.ts:703—saveItem: async (type, name, item)→PUT ${route}/${type}/${name}, body only.packages/client/src/index.ts:5151— the scoped twin →PUT /meta/${type}/${name}, body only.Measured on
origin/main@11038529:grep -c force packages/client/src/index.tsis 9, and not one of those hits is on a metadata save — they areenvironments.delete({ force })(?force=1), unrelated prose, and the word "enforced". The siblinggetItemon the same object does compose a query string (if (options?.packageId) params.set('package', …)), so this is an absent option on one method rather than a client that cannot send query parameters.So an SDK caller refused with the destructive 409 is told to re-submit with a parameter their client offers no way to set. The remedy is reachable only by dropping to raw
fetch.Why this is not #11095
#11095 was about two ROUTES that did not read a parameter callers could send. This is one CLIENT that cannot send a parameter both routes now read — a different layer, a different fix, and a different decision. It also predates #11095 on the single-segment door: the clause has been unactionable for SDK callers there the whole time, so this is not a regression that card introduced.
It is equally absent on both doors, so it is not a twin divergence either, and #7019's "twin doors must not diverge" argument — the load-bearing one for #11095's compound-name half — says nothing about it.
Why it is a contract question rather than an obvious fix
Adding
forcetometa.saveItemwidens the published SDK surface, which is the same tier of decision #11095 was escalated for. Candidate shapes, none of them free:saveItem(type, name, item, { force?: boolean }). MatchesgetItem's existingoptions?.packageIdshape on the same object, and leaves room for the twin's other two unexposed query parameters (?package,?mode=draft), which are in exactly the same position. Widest, and the one that makes the whole door's surface reachable.forcealone, narrowest, on the argument that only this parameter is named by a refusal the caller is expected to act on. Leaves?package/?modeunreachable and invites a second card.?force=trueon routes that never thread it — the compound-namePUT /meta/:type/:a/:band the runtime dispatcherPUT /meta#11095 just removed.Worth measuring before choosing, and not measured here: whether Studio and the CLI go through
meta.saveItemor through raw HTTP for metadata saves. If the real destructive-edit surfaces already bypass the SDK, option C's cost is much lower than it looks, and option A is speculative surface.Where the repair would land
packages/client/src/index.ts(bothsaveItemdeclarations, which must stay in step — they are the same method on two clients), plus whatever pinsclient.test.tsholds on the URL those methods build.Related: #11095 (the routes, fixed), #11015 (the face-aware remedy clause), #10886 (the sole-carrier verdict on this message).
Measured on
origin/mainat11038529.