Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .changeset/register-remaining-boot-refusal-codes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
"@objectstack/spec": minor
---

feat(spec): register the fourteen remaining `door: 'none'` error codes that ship in `dist` — the rest of the #16404 class after #16449 enters `ERROR_CODE_LEDGER` (#16649)

Under the #16404 ruling (director seat, decision batch #62, 2026-09-07, option D; maintainer 「同意」) **the published contract face for error codes is `ERROR_CODE_LEDGER` / `StandardErrorCode`**: every `code` that ships in a package's `dist` is registered there, door or no door, because a consumer's `catch (e) { switch (e.code) }` pins the spelling the moment it ships. #16449 registered the nine codes measured on its tree; fourteen more were still shipping unregistered — every `boot-refusal` row `dispatcher-error-vocabulary.ts` carried — and now have rows, each under the package that stamps it:

| code | stamped by | `status` on the thrown value | reaches an HTTP door on this tree? |
|---|---|---|---|
| `INVALID_ARTIFACT_PACKAGES` · `INVALID_ARTIFACT_PACKAGE_ENTRY` · `DUPLICATE_ARTIFACT_PACKAGE` | `@objectstack/core` (`resolveArtifactPackageOrder`, ADR-0130 D4/D5) | 422 | no — boot-time `manifest.register()` aborts boot; the install route answers with its own `PLUGIN_REGISTER_FAILED` |
| `NO_SUCH_RUN` · `PLAN_CHANGED` · `PREFLIGHT_FAILED` · `NOT_COMPENSABLE` | `@objectstack/core` (`MigrationJournalRefusal`, the migration-journal runner) | none | no — caught by the CLI's `migrate` commands with `instanceof` and printed |
| `SERVICE_NOT_REGISTERED` | `@objectstack/core` (`PluginLoader.getService`'s "never registered" rejection) | none, by design | no — read in-process by the seam that catches the rejection |
| `PLUGIN_CONTRACT_VIOLATION` | `@objectstack/core` (`assertPluginContract`, raised at `kernel.use()`) | none | no — raised while the kernel is still registering plugins |
| `MIXED_ARTIFACT_COLLECTION_SHAPE` | `@objectstack/runtime` (`resolveArtifactCollections`, ADR-0130 D4) | 422 | no — every call site resolves before a transport exists |
| `DUPLICATE_ARTIFACT_OBJECT_NAME` | `@objectstack/objectql` (`SchemaRegistry.installPackage`, ADR-0130 D3) | 422 | no — the HTTP install sites pass no artifact scope, so they cannot raise it |
| `MEMORY_MULTI_TENANT_UNSUPPORTED` | `@objectstack/driver-memory` (the tenancy guard) | none | no — a boot refusal the CLI rethrows pre-HTTP |
| `MONGODB_MULTI_TENANT_UNSUPPORTED` | `@objectstack/driver-mongodb` (the tenancy guard) | none | no — a boot refusal the CLI rethrows pre-HTTP (registered by #3724, unregistered by #8035, re-registered here under the ruling) |
| `WALLED_MEMBERSHIP_POLICY_UNDECLARED` | `@objectstack/organizations` (the walled-posture membership-policy gate, `kernel:bootstrapped`) | none | no — fires before `kernel:listening` opens the socket |

**Wire consequence, stated plainly.** Registration changes what a client reads only where a code reaches an HTTP door: `error.code` would carry the specific code instead of the standard member the status derives, with the producer's spelling no longer demoted into `declaredCode`. Re-measured on this tree at the sites each `boot-refusal` row named (the table's last column is that reading, one line per group), **none of the fourteen has such a door**, so **no HTTP body changes with this release**. What changes is the face: `ErrorCode` — the union `ApiErrorSchema.code` parses against — gains fourteen members, `REGISTERED_ERROR_CODES` lists them, the ledger gains two owner keys (`@objectstack/driver-mongodb` returns after #8035 removed it; `@objectstack/organizations` is new), and each refusal's `e.code` is now a member of the union a consumer's exhaustive `switch` is written over. Should a door ever answer with one of them, the wire carries the specific code from then on.

**`MONGODB_MULTI_TENANT_UNSUPPORTED` is a deliberate reversal, not drift.** #8035 unregistered it on the ground that "host boot matching is not wire vocabulary"; the #16404 ruling supersedes exactly that ground (the ledger header's "Retiring a code" section records both halves), and the test that pinned its absence now pins its presence. What still retires a row is a code with no producer left in `packages/**` — `OVERLAY_PERSISTENCE_FAILED` (#5783) remains the pinned witness of that class.

**Why `minor`, and no `BREAKING` banner.** Nothing is removed or renamed; every existing body parses exactly as before. The change is a purely additive widening of a published surface (fourteen new `ErrorCode` members, two new owner keys), which the 2026-09-04 ruling on #15294 requires to be at least `minor`. The one consumer-visible cost is type-level: an exhaustive `switch` over the `ErrorCode` TYPE gains fourteen cases to cover.

The fourteen `boot-refusal` classification rows in `dispatcher-error-vocabulary.ts` ratchet out with the registrations (the gate reports a registered code's row as `stale-row`), their reachability reading now carried on the ledger rows; that module is not part of `@objectstack/runtime`'s published entry, so nothing in that package's `dist` moves. The `boot-refusal` verdict itself stays declared for a future pre-HTTP producer; retiring it and widening the gate's spec-face refusal to every published package is the card's second half and is not in this release.
16 changes: 15 additions & 1 deletion content/docs/references/api/contract.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const result = ApiErrorSchema.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +308 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +322 more>` | ✅ | Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
| **declaredCode** | `string` | optional | The producer-declared code, verbatim, when it is not a member of the closed `code` vocabulary — the open, author-authored channel (app-specific spellings; ADR-0112) |
| **message** | `string` | ✅ | Readable error message |
| **userMessage** | `string` | optional | Producer-marked user-facing refusal text, verbatim. Present exactly when the producer opted in at throw time; consumers render it to end users and keep their generic substitution for anything unmarked. Status-agnostic; never replaces `message`. |
Expand Down Expand Up @@ -141,6 +141,8 @@ const result = ApiErrorSchema.parse(data);
* `DOMAIN_VERIFICATION_DISABLED`
* `DOMAIN_VERIFICATION_FAILED`
* `DRIVER_UNAVAILABLE`
* `DUPLICATE_ARTIFACT_OBJECT_NAME`
* `DUPLICATE_ARTIFACT_PACKAGE`
* `DUPLICATE_REQUEST`
* `DUPLICATE_SOURCE_NOT_A_BASE`
* `ELIGIBILITY_UNEVALUABLE`
Expand Down Expand Up @@ -195,6 +197,8 @@ const result = ApiErrorSchema.parse(data);
* `IMPORT_JOB_CREATE_FAILED`
* `IMPORT_ROW_FAILED`
* `INTERNAL`
* `INVALID_ARTIFACT_PACKAGES`
* `INVALID_ARTIFACT_PACKAGE_ENTRY`
* `INVALID_EMAIL`
* `INVALID_EXPIRY`
* `INVALID_METADATA`
Expand All @@ -221,17 +225,21 @@ const result = ApiErrorSchema.parse(data);
* `MARKETPLACE_PROXY_FAILED`
* `MARKETPLACE_STORAGE_FAILED`
* `MARKETPLACE_UNAVAILABLE`
* `MEMORY_MULTI_TENANT_UNSUPPORTED`
* `METADATA_BRANCH`
* `METADATA_CONFLICT`
* `METADATA_NOT_FOUND`
* `METADATA_SCHEMA_INVALID`
* `MIXED_ARTIFACT_COLLECTION_SHAPE`
* `MONGODB_MULTI_TENANT_UNSUPPORTED`
* `MULTI_UPDATE_HOOK_KEY_DIVERGENCE`
* `NAMESPACE_CONFLICT`
* `NAMESPACE_PREFIX`
* `NEEDS_PASSWORD`
* `NODE_FAILURE`
* `NOTHING_TO_PURGE`
* `NOT_ATTEMPTED`
* `NOT_COMPENSABLE`
* `NOT_CREATABLE`
* `NOT_FOUND`
* `NOT_OVERRIDABLE`
Expand All @@ -241,6 +249,7 @@ const result = ApiErrorSchema.parse(data);
* `NO_IDENTITY`
* `NO_MATCH`
* `NO_PENDING_VERIFICATION`
* `NO_SUCH_RUN`
* `OAUTH_REGISTER_FAILED`
* `OBJECT_API_DISABLED`
* `OBJECT_API_METHOD_NOT_ALLOWED`
Expand All @@ -260,10 +269,13 @@ const result = ApiErrorSchema.parse(data);
* `PAYLOAD_TOO_LARGE`
* `PERMISSION_NOT_ALLOWED`
* `PHONE_NOT_ENABLED`
* `PLAN_CHANGED`
* `PLUGIN_CONTRACT_VIOLATION`
* `PLUGIN_INSTALL_FAILED`
* `PLUGIN_MANIFEST_INVALID`
* `PLUGIN_REGISTER_FAILED`
* `PLUGIN_UI_REQUIRED_KEY_MISSING`
* `PREFLIGHT_FAILED`
* `PROJECT_MEMBERSHIP_REQUIRED`
* `PROJECT_NOT_FOUND`
* `PROJECT_PROVISIONING`
Expand Down Expand Up @@ -302,6 +314,7 @@ const result = ApiErrorSchema.parse(data);
* `SCHEDULES_LIST_FAILED`
* `SCHEDULE_DELETE_FAILED`
* `SELF_REGISTRATION_CLOSED`
* `SERVICE_NOT_REGISTERED`
* `SETTINGS_ACTION_FAILED`
* `SETTINGS_CRYPTO_UNAVAILABLE`
* `SETTINGS_ENGINE_NOT_BOUND`
Expand Down Expand Up @@ -351,6 +364,7 @@ const result = ApiErrorSchema.parse(data);
* `VALIDATION_FAILED`
* `VERSION_NOT_FOUND`
* `VERSION_NOT_RESTORABLE`
* `WALLED_MEMBERSHIP_POLICY_UNDECLARED`
* `WRITABLE_PACKAGE_REQUIRED`
* `WRONG_PASSWORD`

Expand Down
26 changes: 22 additions & 4 deletions content/docs/references/api/error-code-ledger.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -161,10 +161,14 @@ best-effort catch that logs and continues. Its throw site and constant
host boot matching is not wire vocabulary.
⚠️ That SECOND ground is superseded by #16404 ("Door or no door" above): a
boot refusal that ships in `dist` is owed a row, so the codes left out or
retired on the "not wire vocabulary" reasoning — the remaining
`boot-refusal` rows of `dispatcher-error-vocabulary.ts` — are registrations
owed under the ruling, not re-argued per card. What still retires a row is
the FIRST ground only: no producer left anywhere in `packages/**` source.
retired on the "not wire vocabulary" reasoning were registrations owed
under the ruling, not re-argued per card — #16449 registered the nine
measured on its tree, and #16649 the fourteen `boot-refusal` rows
`dispatcher-error-vocabulary.ts` still carried, among them
`MONGODB_MULTI_TENANT_UNSUPPORTED` itself, back under
`@objectstack/driver-mongodb` with the #8035 removal reversed on the
record. What still retires a row is the FIRST ground only: no producer
left anywhere in `packages/**` source.
Before deleting a row, check that no producer remains repo-wide AND
that no consumer — including `objectui` and `cloud` — reads the literal;
tests that merely CONSTRUCT the code are not producers, and a test pinned to
Expand Down Expand Up @@ -296,6 +300,8 @@ const result = ErrorCode.parse(data);
* `DOMAIN_VERIFICATION_DISABLED`
* `DOMAIN_VERIFICATION_FAILED`
* `DRIVER_UNAVAILABLE`
* `DUPLICATE_ARTIFACT_OBJECT_NAME`
* `DUPLICATE_ARTIFACT_PACKAGE`
* `DUPLICATE_REQUEST`
* `DUPLICATE_SOURCE_NOT_A_BASE`
* `ELIGIBILITY_UNEVALUABLE`
Expand Down Expand Up @@ -350,6 +356,8 @@ const result = ErrorCode.parse(data);
* `IMPORT_JOB_CREATE_FAILED`
* `IMPORT_ROW_FAILED`
* `INTERNAL`
* `INVALID_ARTIFACT_PACKAGES`
* `INVALID_ARTIFACT_PACKAGE_ENTRY`
* `INVALID_EMAIL`
* `INVALID_EXPIRY`
* `INVALID_METADATA`
Expand All @@ -376,17 +384,21 @@ const result = ErrorCode.parse(data);
* `MARKETPLACE_PROXY_FAILED`
* `MARKETPLACE_STORAGE_FAILED`
* `MARKETPLACE_UNAVAILABLE`
* `MEMORY_MULTI_TENANT_UNSUPPORTED`
* `METADATA_BRANCH`
* `METADATA_CONFLICT`
* `METADATA_NOT_FOUND`
* `METADATA_SCHEMA_INVALID`
* `MIXED_ARTIFACT_COLLECTION_SHAPE`
* `MONGODB_MULTI_TENANT_UNSUPPORTED`
* `MULTI_UPDATE_HOOK_KEY_DIVERGENCE`
* `NAMESPACE_CONFLICT`
* `NAMESPACE_PREFIX`
* `NEEDS_PASSWORD`
* `NODE_FAILURE`
* `NOTHING_TO_PURGE`
* `NOT_ATTEMPTED`
* `NOT_COMPENSABLE`
* `NOT_CREATABLE`
* `NOT_FOUND`
* `NOT_OVERRIDABLE`
Expand All @@ -396,6 +408,7 @@ const result = ErrorCode.parse(data);
* `NO_IDENTITY`
* `NO_MATCH`
* `NO_PENDING_VERIFICATION`
* `NO_SUCH_RUN`
* `OAUTH_REGISTER_FAILED`
* `OBJECT_API_DISABLED`
* `OBJECT_API_METHOD_NOT_ALLOWED`
Expand All @@ -415,10 +428,13 @@ const result = ErrorCode.parse(data);
* `PAYLOAD_TOO_LARGE`
* `PERMISSION_NOT_ALLOWED`
* `PHONE_NOT_ENABLED`
* `PLAN_CHANGED`
* `PLUGIN_CONTRACT_VIOLATION`
* `PLUGIN_INSTALL_FAILED`
* `PLUGIN_MANIFEST_INVALID`
* `PLUGIN_REGISTER_FAILED`
* `PLUGIN_UI_REQUIRED_KEY_MISSING`
* `PREFLIGHT_FAILED`
* `PROJECT_MEMBERSHIP_REQUIRED`
* `PROJECT_NOT_FOUND`
* `PROJECT_PROVISIONING`
Expand Down Expand Up @@ -457,6 +473,7 @@ const result = ErrorCode.parse(data);
* `SCHEDULES_LIST_FAILED`
* `SCHEDULE_DELETE_FAILED`
* `SELF_REGISTRATION_CLOSED`
* `SERVICE_NOT_REGISTERED`
* `SETTINGS_ACTION_FAILED`
* `SETTINGS_CRYPTO_UNAVAILABLE`
* `SETTINGS_ENGINE_NOT_BOUND`
Expand Down Expand Up @@ -506,6 +523,7 @@ const result = ErrorCode.parse(data);
* `VALIDATION_FAILED`
* `VERSION_NOT_FOUND`
* `VERSION_NOT_RESTORABLE`
* `WALLED_MEMBERSHIP_POLICY_UNDECLARED`
* `WRITABLE_PACKAGE_REQUIRED`
* `WRONG_PASSWORD`

Expand Down
Loading
Loading