Repository navigation
fix(objectql): an uninstall refused because another package extends one of its objects answers 409 RESOURCE_CONFLICT with the way out, not 500 INTERNAL_ERROR - #23522
Conversation
…n owned object answers 409 RESOURCE_CONFLICT with the way out SchemaRegistry.assertPackageUninstallable threw a bare Error, so DELETE /api/v1/packages/:id fell through to its 500 INTERNAL_ERROR fallback for a client-actionable state. The refusal now carries the ADR-0112 envelope (RESOURCE_CONFLICT / 409), opens with the way out (uninstall the extending packages first) and names every extended object and extending package. Claude-Session: https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm Co-authored-by: Claude <noreply@anthropic.com>
…tension-refusal-409
📓 Docs Drift CheckThis PR changes 1 package(s): 25 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 17 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 f7b034e9686d927fbe023993afafb2274ed488b6 && git checkout f7b034e9686d927fbe023993afafb2274ed488b6
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 222293f60a7fff2779269b20bb4e5ce28a34c935 fdc2dc05fc4c8029dad8cdc1c2f9b9e2c7d9cfe1 && git checkout -B drift-repro 222293f60a7fff2779269b20bb4e5ce28a34c935 && git merge --no-ff fdc2dc05fc4c8029dad8cdc1c2f9b9e2c7d9cfe1
node scripts/docs-audit/affected-docs.mjs --json 222293f60a7fff2779269b20bb4e5ce28a34c935
|
席位核对
Verdict: ACCEPT. Not governed (
|
Fixes #23505
Clause-②: no
Dispatched by the
domain:specseat 4 PM (sunhaolin) to sessionsession_0121ooBYRmS8ziFXm9ai5YHm, branchclaude/issue-23505-extension-refusal-409, claim comment 6172782630.What changed
SchemaRegistry.assertPackageUninstallable(packages/objectql/src/registry.ts) threw a bareErrorwith nocodeand nostatuswhen the package owns an object another package extends (ADR-0029).deletePackagestep 3 rethrows it as is, soDELETE /api/v1/packages/:idfell throughresolveThrownHttpErrorto its500 INTERNAL_ERRORfallback. The caller could change the state that blocked the uninstall, but the answer said the server failed.The fix is at the producer. The refusal is now an unexported
PackageUninstallExtendedError, in the registry's house shape (code,statusandhttpStatus, likeNamespaceConflictErrorandObjectOwnershipConflictError):code: 'RESOURCE_CONFLICT',status: 409. No new code: this is the standard catalog member that the verb's sibling refusals already answer for environment state (dropStorageRemainingRowsRefusal, and the shared-object pre-flight in PR fix(metadata-protocol)!: an uninstall that would drop a table another stored row still serves is refused 409 before anything is removed #23504).The way out comes first, because a 4xx message can be truncated. The diagnostic sentence the refusal always carried follows, and it now names every extended object (the first five, then a count of the rest), each with its extenders in registration order. Measured at the door:
Structured fields for in-process callers:
packageId,extendedObjects,extenders.The decision itself is unchanged: a package is refused in the same cases as before, and before any mutation.
packages/metadata-protocol/src/protocol.tsis not edited. Step 3 already rethrows the registry's error as is, so the door gets the status without touching steps 1–4, which PR #23504 holds.Every caller, measured
deletePackagestep 3, thenDELETE /api/v1/packages/:id(errorFromThrown(e, 500))500 INTERNAL_ERRORbecomes409 RESOURCE_CONFLICTwith the prescription. Nothing changes in storage or the registry (pinned below).registry.uninstallPackage(id)itself; terminalerrorFromThrown)uninstallPackage500to409 RESOURCE_CONFLICT. The package and its object stay registered. Measured with a one-off probe, deleted after reading. Not pinned.deletePackagestep 6console.warnwith the message@objectstack/cloud-connectioninstall-local withdrawalerr.messageonlycleanupsoutcome is unchanged; only the log text changes.MetadataFacade.unregisterPackage,ObjectQL.unregisterObject(name, packageId)git grep).ObjectQL.removePackageforce: trueTests
Every reading below is at
fdc2dc05fc(this branch after mergingorigin/mainat222293f60a), except the full objectql run, which is noted.packages/runtime/src/package-uninstall-store-refusal.integration.test.ts(the existing pin). It runs on a real stack:ObjectQLoverSqlDriver,PackageServicePlugin, the protocol andHttpDispatcher, booted twice over one SQLite file.{ status: 500, code: 'INTERNAL_ERROR' }to{ status: 409, code: 'RESOURCE_CONFLICT' }. It also asserts the opening of the prescription and the object's name.409. ThenDELETEof the extender through the same door, and the same uninstall answers200and stays gone after a restart.packages/objectql/src/registry-assert-package-uninstallable.test.tspins:unregisterObjectsByPackageanduninstallPackage.packages/objectql/src/registry.test.ts: the order pin added for the refusal-pass ordering fix now reads the order off the structured refusal (first refusable object first, its extenders in registration order) instead of the old sentence.package-uninstall-*+domains/packages-*package-uninstall-org-scoperegistry*.test.ts+ the two objectql tests the merge broughtdeletePackagetestsvitest run --project local(ata9a88b1e34; the merge changed no objectql source, only the two tests re-run above)pnpm --filter @objectstack/objectql typecheck, same for runtimeAblation (the producer's
codeandstatusrenamed away)The mutation went through
scripts/ablation-replace.mjs(anchor hit 1 time; blob69df6079e5a7to7d89ea3120fa). objectql was rebuilt, andablation-dist-preflightfound the planted marker in 4 dist files.{ status: 409, code: 'RESOURCE_CONFLICT' }and received{ status: 500, code: 'INTERNAL_ERROR' }, which is the pre-fix answer.expected 500 to be 409).registry-assert-package-uninstallable.test.ts: red.git diff HEADis empty andgit status --porcelainis empty. After an objectql rebuild,--absentfound the marker in 0 of 14 dist files, and the pin passed 5/5 again.Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ranatfdc2dc05fc: 68 derived, 67 run withexit 0, 1 NOT-MEASURED, 0 UNRUN.pnpm check:dual-build-cjs-loads. It answeredPREREQUISITE NOT MET(exit 3) because it reads every package'sdist/and no full workspace build was run locally. CI runs it.check:dispatcher-error-vocabularyis green.RESOURCE_CONFLICTis a standard member, so no table row is owed.check:error-status-conformanceis green. Its deriver recordsRESOURCE_CONFLICT/409 fromclass PackageUninstallExtendedError; I measured this by callingderiveRuntimeStatusesonregistry.ts, because the report shows only four sites per code.check-adr-0087-registrationreports "no declared-breaking changeset (1 non-breaking changeset(s) seen)".check-changeset-no-majorandcheck-empty-changesetare green.check:error-code-provenanceandcheck:error-code-casing, bothexit 0.The changeset is
.changeset/23505-extension-refusal-409.md:@objectstack/objectqlpatch, with FROM, TO and Unchanged sections.Acceptance notes
500where the door answers409. Both are outside this claim's file surface and change no behaviour:packages/metadata-protocol/src/protocol.ts,deletePackagestep 3: "The HTTP door answers it through itscatcharound this verb:500, nothing changed." PR fix(metadata-protocol)!: an uninstall that would drop a table another stored row still serves is refused 409 before anything is removed #23504 holds that block.packages/runtime/src/domains/packages.ts, in the DELETE arm: "which thecatchabove answers —500, nothing changed."409 RESOURCE_CONFLICT, the refusal's own envelope, nothing changed. Carrier: none.packages/metadata-protocol/src/protocol.package-delete-refusal.test.tsbuilds its fake refusal with the old sentence. It pins pass-through identity, not the text, so it stays valid.500to409too. It was measured, not pinned.Generated by Claude Code