Measured while repairing the remedy clause on the duplicate face for #11015. Separate defect, filed rather than fixed in that PR — the repair there is not obviously the repair here.
What was measured
saveMetaItem's Phase 3a-destructive gate raises the 409 DESTRUCTIVE_CHANGE in one place and the clause is quoted onto whatever response the caller's catch builds. #11015 made that clause face-aware and repaired the one face it was filed on (duplicatePackage's failed[].error). The face inventory in packages/metadata-protocol/src/protocol.destructive-409-face-inventory.test.ts lists two further doors that reach the same gate and also never thread force, and both were re-measured on origin/main @ 4257e4e:
| face |
call site |
threads force? |
@objectstack/rest PUT /meta/:type/:name |
packages/rest/src/rest-server.ts:5472 |
yes — ...(force ? { force: true } : {}) |
@objectstack/rest PUT /meta/:type/:a/:b (compound name) |
packages/rest/src/rest-server.ts:6590 |
no — the request is built field by field and force is not one of them |
@objectstack/runtime dispatcher PUT /meta |
packages/runtime/src/domains/meta.ts:417 |
no — saveMetaItem({ type, name, item, organizationId, ...(packageId ? { packageId } : {}) }) |
So a caller refused on either of the bottom two rows is told — re-submit with ?force=true to proceed. and, doing exactly that, gets the identical refusal back. Same defect class as #11015, different doors.
Why this was not folded into #11015's PR
#11015's duplicate face had a genuine alternative to prescribe (duplicate into a free target namespace, or reconcile the collision first), so repairing the message was the whole fix and widened nothing. These two doors have no such alternative: they address one item by name, and the only thing that lifts a risk-acknowledgement refusal is the acknowledgement. The two candidate repairs are therefore
Both are defensible and the choice is not the dev seat's to make, which is why this is filed rather than folded.
Where the repair would land
packages/metadata-protocol/src/protocol.ts already carries the mechanism #11015 added — destructiveChangeRemedy(face, name) plus a server-stated writeFace on the saveMetaItem request — so option B is a new face value and two call sites stating it. Option A is a change to packages/rest/src/rest-server.ts and packages/runtime/src/domains/meta.ts. The ⚠️ paragraph on destructiveChangeRemedy's docblock and the [#11015] section of the face-inventory test both record these two rows as knowingly left, and point here.
Related: #11015 (the duplicate face, fixed), #10886 (the sole-carrier verdict that forbids trimming the findings prose).
Measured on origin/main at 4257e4e.
Measured while repairing the remedy clause on the duplicate face for #11015. Separate defect, filed rather than fixed in that PR — the repair there is not obviously the repair here.
What was measured
saveMetaItem's Phase 3a-destructive gate raises the409 DESTRUCTIVE_CHANGEin one place and the clause is quoted onto whatever response the caller's catch builds. #11015 made that clause face-aware and repaired the one face it was filed on (duplicatePackage'sfailed[].error). The face inventory inpackages/metadata-protocol/src/protocol.destructive-409-face-inventory.test.tslists two further doors that reach the same gate and also never threadforce, and both were re-measured onorigin/main@4257e4e:force?@objectstack/restPUT /meta/:type/:namepackages/rest/src/rest-server.ts:5472...(force ? { force: true } : {})@objectstack/restPUT /meta/:type/:a/:b(compound name)packages/rest/src/rest-server.ts:6590forceis not one of them@objectstack/runtimedispatcherPUT /metapackages/runtime/src/domains/meta.ts:417saveMetaItem({ type, name, item, organizationId, ...(packageId ? { packageId } : {}) })So a caller refused on either of the bottom two rows is told
— re-submit with ?force=true to proceed.and, doing exactly that, gets the identical refusal back. Same defect class as #11015, different doors.Why this was not folded into #11015's PR
#11015's duplicate face had a genuine alternative to prescribe (duplicate into a free target namespace, or reconcile the collision first), so repairing the message was the whole fix and widened nothing. These two doors have no such alternative: they address one item by name, and the only thing that lifts a risk-acknowledgement refusal is the acknowledgement. The two candidate repairs are therefore
forceon both routes, so the sentence becomes true. This widens a public surface on two HTTP doors and is a contract decision. Note the compound-name route was deliberately kept in step with its single-segment twin once before (#6603's gate does not close #6603: the same masked-schema round-trip deletion is still reachable through the compound-name PUT and the dispatcher PUT (plus an ungated DELETE) #7019, the masked-schema round-trip), which is an argument that the twoPUTdoors should not disagree aboutforceeither.POST /packages/:id/duplicatereports "re-submit with ?force=true to proceed" on a door that accepts noforce— the remedy names a mechanism unavailable on that face #11015 did, stating whatever is actually actionable (reconcile the colliding item first). Message-side only, widens nothing, but prescribes strictly less than option A would.Both are defensible and the choice is not the dev seat's to make, which is why this is filed rather than folded.
Where the repair would land
packages/metadata-protocol/src/protocol.tsalready carries the mechanism #11015 added —destructiveChangeRemedy(face, name)plus a server-statedwriteFaceon thesaveMetaItemrequest — so option B is a new face value and two call sites stating it. Option A is a change topackages/rest/src/rest-server.tsandpackages/runtime/src/domains/meta.ts. The⚠️paragraph ondestructiveChangeRemedy's docblock and the[#11015]section of the face-inventory test both record these two rows as knowingly left, and point here.Related: #11015 (the duplicate face, fixed), #10886 (the sole-carrier verdict that forbids trimming the findings prose).
Measured on
origin/mainat4257e4e.