diff --git a/.changeset/register-flow-conversion-conflict.md b/.changeset/register-flow-conversion-conflict.md new file mode 100644 index 0000000000..a15c10d392 --- /dev/null +++ b/.changeset/register-flow-conversion-conflict.md @@ -0,0 +1,13 @@ +--- +"@objectstack/spec": patch +--- + +Register `FLOW_CONVERSION_CONFLICT` (409) in the ADR-0112 error-code ledger under +`@objectstack/metadata-protocol` (#9567). The code was already live on the wire — +`saveMetaItem`'s flow-conversion rename guard (`protocol.ts`) has thrown it since +ADR-0078 landed, already SCREAMING_SNAKE — but was invisible to +`check:dispatcher-error-vocabulary`'s scan because the site stamps it through a +cast (`(err as any).code = 'FLOW_CONVERSION_CONFLICT'`) rather than the bare- +identifier `assign` shape the scan matched at the time. This is an ordinary, +additive admission: no accept/reject behavior, no producer, and no wire shape +changes. diff --git a/content/docs/references/api/contract.mdx b/content/docs/references/api/contract.mdx index 1b939734cb..2611dc36ba 100644 --- a/content/docs/references/api/contract.mdx +++ b/content/docs/references/api/contract.mdx @@ -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' \| … +285 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' \| … +286 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, #9106) | | **message** | `string` | ✅ | Readable error message | | **category** | `string` | optional | Error category (e.g. validation, authorization) | @@ -179,6 +179,7 @@ const result = ApiErrorSchema.parse(data); * `FILE_NOT_FOUND` * `FILTER_TOKEN_UNKNOWN` * `FILTER_TOKEN_UNRESOLVED` +* `FLOW_CONVERSION_CONFLICT` * `FLOW_DISABLED` * `FLOW_FAILED` * `FLOW_NO_START_NODE` diff --git a/content/docs/references/api/error-code-ledger.mdx b/content/docs/references/api/error-code-ledger.mdx index fe689d95a3..b4a2675b32 100644 --- a/content/docs/references/api/error-code-ledger.mdx +++ b/content/docs/references/api/error-code-ledger.mdx @@ -284,6 +284,7 @@ const result = ErrorCode.parse(data); * `FILE_NOT_FOUND` * `FILTER_TOKEN_UNKNOWN` * `FILTER_TOKEN_UNRESOLVED` +* `FLOW_CONVERSION_CONFLICT` * `FLOW_DISABLED` * `FLOW_FAILED` * `FLOW_NO_START_NODE` diff --git a/packages/runtime/src/dispatcher-error-vocabulary.ts b/packages/runtime/src/dispatcher-error-vocabulary.ts index 971f7588f8..05e0e29603 100644 --- a/packages/runtime/src/dispatcher-error-vocabulary.ts +++ b/packages/runtime/src/dispatcher-error-vocabulary.ts @@ -216,7 +216,12 @@ export const UNREGISTERED_CODE_SITES: readonly UnregisteredCodeSite[] = [ // UNIQUE_SCOPE_CONFIRMATION_REQUIRED at the marketplace install seam's // `plugin-route` door (stamped through a constant in an object literal, the // shape `objlit` could not see), and #9246 registered it under - // `@objectstack/cloud-connection`, ratcheting that row out the same way. A + // `@objectstack/cloud-connection`, ratcheting that row out the same way. + // #9460's further-widened scan then reported two sites at once — + // `FLOW_CONVERSION_CONFLICT` and `owd_widening_forbidden` — and #9567 + // registered the former under `@objectstack/metadata-protocol`, ratcheting + // only that row out; `owd_widening_forbidden`'s lowercase spelling is a + // naming decision, not a plain admission, so it stays pending below. A // future unswept producer lands here as an `unclassified-site` finding and // gets a new row (then a spec-lane registration, then the row comes out // again). ── @@ -357,25 +362,14 @@ export const UNREGISTERED_CODE_SITES: readonly UnregisteredCodeSite[] = [ }, // ── pending registration [#9460]: found by the widened scan ──────────── - // Both rows are the deliverable of #9460, not a regression: the scan could - // not SEE either site before it learned the two stamp positions below, so - // "no finding" meant "not looked at", which is the failure this gate - // exists to prevent. - { - code: 'FLOW_CONVERSION_CONFLICT', - file: 'packages/metadata-protocol/src/protocol.ts', - shape: 'assign', - door: 'rest', - verdict: 'pending-registration', - why: - 'A live 409 from the metadata write path: the conversion pass refuses a body whose token is a ' + - "live name in the environment, and stamps `(err as any).code = 'FLOW_CONVERSION_CONFLICT'` " + - 'beside `status = 409` before throwing. [#9460] The `assign` shape demanded a BARE identifier ' + - 'to the left of `.code`, and this site writes a cast — `(err as any)` puts a `)` exactly where ' + - 'the anchor wanted a word character — so the single most common way this repo stamps a code ' + - 'onto a constructed error was invisible in the shape named for it. Registering it is the ' + - "`packages/spec` lane's call (#8846's batch); this row records the measurement.", - }, + // This row is the deliverable of #9460, not a regression: the scan could + // not SEE the site before it learned the stamp shape below (a + // code-carrying helper), so "no finding" meant "not looked at", which is + // the failure this gate exists to prevent. Its sibling from the same + // #9460 batch, `FLOW_CONVERSION_CONFLICT`, ratcheted out via #9567 (see + // the running log above); this one stays pending because admitting it AS + // SPELLED is what ADR-0112 D1 forbids — the rename-or-keep-the-#9106-demote + // call is the spec lane's and is not resolved here. { code: 'owd_widening_forbidden', file: 'packages/plugins/plugin-security/src/object-posture-gate.ts', diff --git a/packages/spec/src/api/error-code-ledger.zod.ts b/packages/spec/src/api/error-code-ledger.zod.ts index 4dc5698c02..38119239ce 100644 --- a/packages/spec/src/api/error-code-ledger.zod.ts +++ b/packages/spec/src/api/error-code-ledger.zod.ts @@ -377,6 +377,19 @@ export const ERROR_CODE_LEDGER = { 'COMMIT_NOT_FOUND', 'CONCURRENT_UPDATE', 'DESTRUCTIVE_CHANGE', // change would drop data; needs an explicit opt-in + // [#9567] ADR-0078's rename guard refuses a metadata write whose `type`/ + // `name` token is already a LIVE name owned by something else in the + // environment — persisting the un-renamed body would mint exactly the row + // that guard exists to prevent (`saveMetaItem`, `protocol.ts`, same posture + // as `duplicatePackage` / #4454). 409, not 422: the body may be perfectly + // valid — the refusal comes from environment state, so resubmitting the + // same body cannot help. Surfaced by the widened + // `check:dispatcher-error-vocabulary` scan (#9460, half 1); NOTE the + // producer's message prefix spells the code lowercase + // (`[flow_conversion_conflict]`) while the wire stamp is SCREAMING_SNAKE — + // a human-log inconsistency worth a glance, left as-is because the + // producer is outside this ledger's surface. + 'FLOW_CONVERSION_CONFLICT', 'INVALID_METADATA', 'INVALID_REQUEST', 'ITEM_LOCKED', // _lock refuses the write/delete (ADR-0010 §3.3)