From 57ac54b8d1c297cd93603b2c72bb732d401055b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 12:01:47 +0000 Subject: [PATCH 1/2] feat(spec): register FLOW_CONVERSION_CONFLICT in the ADR-0112 ledger (#9567 half 1) `saveMetaItem`'s flow-conversion rename guard (protocol.ts:12674-12682) has thrown a live 409 FLOW_CONVERSION_CONFLICT since ADR-0078 landed -- already SCREAMING_SNAKE, so this is an ordinary ADR-0112 D3 ledger admission under @objectstack/metadata-protocol. It was invisible to check:dispatcher-error-vocabulary's scan because the site stamps the code through a cast rather than the bare-identifier `assign` shape the scan matched. Regenerated the two docs that mirror the ledger (content/docs/references/api/ contract.mdx, error-code-ledger.mdx) via gen:schema + gen:docs; check:generated and check:docs both confirm no other artifact moved. Half 2 of #9567 (`owd_widening_forbidden`) is measurement-only in this PR -- see the PR description -- so this PR does not close the issue. Part of #9567 --- .changeset/register-flow-conversion-conflict.md | 13 +++++++++++++ content/docs/references/api/contract.mdx | 3 ++- content/docs/references/api/error-code-ledger.mdx | 1 + packages/spec/src/api/error-code-ledger.zod.ts | 13 +++++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 .changeset/register-flow-conversion-conflict.md 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/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) From 8a3b4b4ae9bbfadcc25af66e643d5b8e0c172bdd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 18 Aug 2026 13:34:24 +0000 Subject: [PATCH 2/2] fix(runtime): discharge FLOW_CONVERSION_CONFLICT's pending-registration row (#9567) #9460 landed on main (PR #9573) while this branch's registration PR was in the merge queue, adding a pending-registration row for FLOW_CONVERSION_CONFLICT to dispatcher-error-vocabulary.ts's UNREGISTERED_CODE_SITES. That collided with this branch's ledger admission of the same code -- a registered code with a pending-registration row still on file fails the #8087 conformance gate in the direction the row's own docs describe ("a pending-registration row whose code is registered fails the gate in the other direction"). Ratchets the row out following the #8846/#9246 precedent exactly: delete the discharged row, fold a one-line note into the running log at the top of UNREGISTERED_CODE_SITES. owd_widening_forbidden -- the #9460 batch's other new row -- stays pending; its lowercase spelling is a naming decision for the maintainer (#9567 half 2), not a plain admission, and is untouched. node scripts/check-dispatcher-error-vocabulary.mjs: PENDING_LEDGER_REGISTRATION 2 -> 1 (only owd_widening_forbidden remains). error-envelope.conformance.test.ts (the #8087 pin): 51/51 passing. error-code-ledger.test.ts: 17/17 passing. check:generated: all 13 artifacts still up to date after the merge. Part of #9567 --- .../src/dispatcher-error-vocabulary.ts | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) 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',