Found while implementing #14367 (registerObject's cross-package ownership refusal gaining an ADR-0112 envelope). Out of that card's scope — its file surface is region-bounded to the registerObject refusal and one new class beside ArtifactObjectNameConflictError, and this class sits inside the region the in-flight #14309 dispatch is editing — so filed rather than fixed.
The site
NamespaceConflictError in packages/objectql/src/registry.ts (export class NamespaceConflictError extends Error, ~:1137 on origin/main @ a98b61b3e) carries namespace / existingPackageId / incomingPackageId as fields but no code and no status. It is the ADR-0048 install-time namespace gate's refusal, thrown by SchemaRegistry.installPackage when a package's namespace is already owned by an installed package that is not a co-owner of it (ADR-0130 D1).
Why it matters — reading, not runtime measurement
Unlike the #14367 refusal, installPackage IS on an HTTP path: POST /packages in packages/runtime/src/domains/packages.ts calls protocol.installPackage (or registry.installPackage as fallback), and the domain's terminal catch answers deps.errorFromThrown(e, 500). resolveThrownHttpError (packages/types/src/thrown-http-error.ts) reads .status / .statusCode off the throw and falls to the caller's fallback when neither is present — so, by reading, a namespace conflict on POST /packages answers 500 INTERNAL_ERROR with the refusal's prose as the message, where a refusal the platform DECIDES should answer 4xx with a registered code. Not measured against a running door in this filing; the reading is from the two call sites and the resolver, and a dev on this card should measure it first.
What the sibling refusals already do
DUPLICATE_ARTIFACT_PACKAGE / INVALID_ARTIFACT_PACKAGE_ENTRY (packages/objectql/src/artifact-packages.ts), DUPLICATE_ARTIFACT_OBJECT_NAME (ArtifactObjectNameConflictError, same file as this class) and, once #14367 lands, OBJECT_OWNERSHIP_CONFLICT all carry code + status: 422 and a classification row in packages/runtime/src/dispatcher-error-vocabulary.ts. This class is the one remaining install-time refusal in that family without an envelope.
Existing tests on this path
registry-namespace-install-gate.test.ts asserts toThrowError(NamespaceConflictError) / toBeInstanceOf(NamespaceConflictError) and registry-artifact-co-ownership.test.ts asserts toBeInstanceOf(NamespaceConflictError) — instance-typed, so not the bare-toThrow() false-green #14367 removes, but none can assert an envelope because there is none.
Suggested shape (not prescriptive)
Add code (SCREAMING_SNAKE, e.g. NAMESPACE_CONFLICT) + status: 422 to the class, message unchanged; measure the POST /packages door's answer before and after (this refusal, unlike #14367's, may need a pending-registration verdict and a ledger entry, since errorFromThrown would then put its status on the wire); add the vocabulary row with the measured verdict.
Blocked-by: none (the pattern exists; #14367 only shares the file).
Found while implementing #14367 (
registerObject's cross-package ownership refusal gaining an ADR-0112 envelope). Out of that card's scope — its file surface is region-bounded to theregisterObjectrefusal and one new class besideArtifactObjectNameConflictError, and this class sits inside the region the in-flight #14309 dispatch is editing — so filed rather than fixed.The site
NamespaceConflictErrorinpackages/objectql/src/registry.ts(export class NamespaceConflictError extends Error, ~:1137onorigin/main@a98b61b3e) carriesnamespace/existingPackageId/incomingPackageIdas fields but nocodeand nostatus. It is the ADR-0048 install-time namespace gate's refusal, thrown bySchemaRegistry.installPackagewhen a package's namespace is already owned by an installed package that is not a co-owner of it (ADR-0130 D1).Why it matters — reading, not runtime measurement
Unlike the #14367 refusal,
installPackageIS on an HTTP path:POST /packagesinpackages/runtime/src/domains/packages.tscallsprotocol.installPackage(orregistry.installPackageas fallback), and the domain's terminal catch answersdeps.errorFromThrown(e, 500).resolveThrownHttpError(packages/types/src/thrown-http-error.ts) reads.status/.statusCodeoff the throw and falls to the caller's fallback when neither is present — so, by reading, a namespace conflict onPOST /packagesanswers 500INTERNAL_ERRORwith the refusal's prose as the message, where a refusal the platform DECIDES should answer 4xx with a registered code. Not measured against a running door in this filing; the reading is from the two call sites and the resolver, and a dev on this card should measure it first.What the sibling refusals already do
DUPLICATE_ARTIFACT_PACKAGE/INVALID_ARTIFACT_PACKAGE_ENTRY(packages/objectql/src/artifact-packages.ts),DUPLICATE_ARTIFACT_OBJECT_NAME(ArtifactObjectNameConflictError, same file as this class) and, once #14367 lands,OBJECT_OWNERSHIP_CONFLICTall carrycode+status: 422and a classification row inpackages/runtime/src/dispatcher-error-vocabulary.ts. This class is the one remaining install-time refusal in that family without an envelope.Existing tests on this path
registry-namespace-install-gate.test.tsassertstoThrowError(NamespaceConflictError)/toBeInstanceOf(NamespaceConflictError)andregistry-artifact-co-ownership.test.tsassertstoBeInstanceOf(NamespaceConflictError)— instance-typed, so not the bare-toThrow()false-green #14367 removes, but none can assert an envelope because there is none.Suggested shape (not prescriptive)
Add
code(SCREAMING_SNAKE, e.g.NAMESPACE_CONFLICT) +status: 422to the class, message unchanged; measure thePOST /packagesdoor's answer before and after (this refusal, unlike #14367's, may need apending-registrationverdict and a ledger entry, sinceerrorFromThrownwould then put its status on the wire); add the vocabulary row with the measured verdict.Blocked-by: none (the pattern exists; #14367 only shares the file).