Skip to content

Commit 079b457

Browse files
os-elonclaude
andauthored
feat(spec): retire BATCH_PARTIAL_FAILURE, BATCH_COMPLETE_FAILURE, TRANSACTION_FAILED from StandardErrorCode (#9723)
* feat(spec): retire BATCH_PARTIAL_FAILURE, BATCH_COMPLETE_FAILURE, TRANSACTION_FAILED from StandardErrorCode ADR-0049 enforce-or-remove applied to the error vocabulary, per maintainer ruling on #9266 (2026-08-18): no producer has ever emitted any of the three; the batch surface reports these conditions per row via ledger-registered ROLLED_BACK / NOT_ATTEMPTED at HTTP 200. Removes the 3 enum members, the 3 catalog entries (+ count 53 -> 50), the 3 unpinned-baseline rows, swaps the two test fixture uses to surviving codes, adds a retirement refusal pin, an ADR-0112 amendment, and the ADR-0087 semantic notification entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016D9wdJR14KKCxz1WgdAzcw * merge origin/main (os-regen artifacts taken from main; regeneration follows) * chore(spec): regenerate references docs on merged tree (post-merge regeneration) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016D9wdJR14KKCxz1WgdAzcw --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6f40ed7 commit 079b457

12 files changed

Lines changed: 103 additions & 52 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
Retire `BATCH_PARTIAL_FAILURE`, `BATCH_COMPLETE_FAILURE` and `TRANSACTION_FAILED` from `StandardErrorCode` (ADR-0112 amendment 2026-08-18, ADR-0049 enforce-or-remove, #9266). Breaking for the error vocabulary: the three spellings now fail `StandardErrorCode` / `ApiErrorSchema` parse. No producer has ever emitted any of them — the batch surface reports these conditions per row instead, with strictly more information.
6+
7+
FROM → TO: `error.code === 'BATCH_PARTIAL_FAILURE' | 'BATCH_COMPLETE_FAILURE' | 'TRANSACTION_FAILED'` (envelope-level, never emitted) → read the per-row `results[].errors[].code` — a rolled-back atomic batch marks each row `ROLLED_BACK`, rows the abort never reached `NOT_ATTEMPTED`, and the causal row keeps its own error (HTTP 200, both codes ledger-registered). One-line fix: delete any branch on the three retired spellings (it never fired) and branch on the per-row codes instead.
8+
9+
<!-- adr-0087: registered standard-error-code-batch-members-retired -->

content/docs/api/error-catalog.mdx

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ description: Complete reference for all ObjectStack error codes with causes, fix
55

66
# Error Code Catalog
77

8-
ObjectStack uses a structured error system with **9 error categories** and **53 standardized error codes**. Every error includes a machine-readable code, HTTP status mapping, and retry guidance.
8+
ObjectStack uses a structured error system with **9 error categories** and **50 standardized error codes**. Every error includes a machine-readable code, HTTP status mapping, and retry guidance.
99

1010
<Callout type="info">
1111
**Source:** `packages/spec/src/api/errors.zod.ts`
@@ -460,25 +460,6 @@ runtime can actually emit.
460460

461461
---
462462

463-
## Batch Operation Errors
464-
465-
### `BATCH_PARTIAL_FAILURE`
466-
**Cause:** Some operations in a batch request succeeded while others failed.
467-
**Fix:** Check the `details` for individual operation results. Retry only the failed operations.
468-
**Retry:** `retry_immediate` (failed operations only)
469-
470-
### `BATCH_COMPLETE_FAILURE`
471-
**Cause:** All operations in the batch request failed.
472-
**Fix:** Check the `details` for root cause. Fix and retry the entire batch.
473-
**Retry:** `retry_backoff`
474-
475-
### `TRANSACTION_FAILED`
476-
**Cause:** A database transaction failed and was rolled back.
477-
**Fix:** Check the `details` for the specific failure. Retry the entire transaction.
478-
**Retry:** `retry_backoff`
479-
480-
---
481-
482463
## Action Errors (`/api/v1/actions`)
483464

484465
Since #3962 `/actions` failures speak HTTP like every other route — the status

content/docs/references/api/contract.mdx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ const result = ApiErrorSchema.parse(data);
2727

2828
| Property | Type | Required | Description |
2929
| :--- | :--- | :--- | :--- |
30-
| **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) |
30+
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +283 more>` || Error code (e.g. VALIDATION_ERROR; StandardErrorCode ∪ the ledger the serving side registers — ERROR_CODE_LEDGER for framework packages) |
3131
| **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) |
3232
| **message** | `string` || Readable error message |
3333
| **category** | `string` | optional | Error category (e.g. validation, authorization) |
@@ -87,9 +87,6 @@ const result = ApiErrorSchema.parse(data);
8787
* `EXTERNAL_SERVICE_ERROR`
8888
* `INTEGRATION_ERROR`
8989
* `WEBHOOK_DELIVERY_FAILED`
90-
* `BATCH_PARTIAL_FAILURE`
91-
* `BATCH_COMPLETE_FAILURE`
92-
* `TRANSACTION_FAILED`
9390
* `ACCOUNT_LOCKED`
9491
* `ALREADY_REVERTED`
9592
* `AMBIGUOUS_MATCH`

content/docs/references/api/error-code-ledger.mdx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -192,9 +192,6 @@ const result = ErrorCode.parse(data);
192192
* `EXTERNAL_SERVICE_ERROR`
193193
* `INTEGRATION_ERROR`
194194
* `WEBHOOK_DELIVERY_FAILED`
195-
* `BATCH_PARTIAL_FAILURE`
196-
* `BATCH_COMPLETE_FAILURE`
197-
* `TRANSACTION_FAILED`
198195
* `ACCOUNT_LOCKED`
199196
* `ALREADY_REVERTED`
200197
* `AMBIGUOUS_MATCH`
@@ -446,7 +443,7 @@ const result = ErrorCode.parse(data);
446443
| Property | Type | Required | Description |
447444
| :--- | :--- | :--- | :--- |
448445
| **code** | `string` || The registered extension code the waiver keeps admissible |
449-
| **shadows** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +46 more>` || The standard-catalog member whose condition the code re-spells |
446+
| **shadows** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +43 more>` || The standard-catalog member whose condition the code re-spells |
450447
| **reason** | `string` || Why the synonym stays registered — recorded so admission is a decision, not drift |
451448

452449
### Allowed Values: `StandardSynonymWaiver.shadows`
@@ -501,9 +498,6 @@ const result = ErrorCode.parse(data);
501498
* `EXTERNAL_SERVICE_ERROR`
502499
* `INTEGRATION_ERROR`
503500
* `WEBHOOK_DELIVERY_FAILED`
504-
* `BATCH_PARTIAL_FAILURE`
505-
* `BATCH_COMPLETE_FAILURE`
506-
* `TRANSACTION_FAILED`
507501

508502

509503
---

content/docs/references/api/errors.mdx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const result = EnhancedApiErrorSchema.parse(data);
4040

4141
| Property | Type | Required | Description |
4242
| :--- | :--- | :--- | :--- |
43-
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +46 more>` || Machine-readable error code |
43+
| **code** | `Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| … +43 more>` || Machine-readable error code |
4444
| **message** | `string` || Human-readable error message |
4545
| **category** | `Enum<'validation' \| 'authentication' \| 'authorization' \| 'not_found' \| 'conflict' \| 'rate_limit' \| 'server' \| 'external' \| 'maintenance'>` | optional | Error category |
4646
| **httpStatus** | `number` | optional | HTTP status code |
@@ -108,9 +108,6 @@ const result = EnhancedApiErrorSchema.parse(data);
108108
* `EXTERNAL_SERVICE_ERROR`
109109
* `INTEGRATION_ERROR`
110110
* `WEBHOOK_DELIVERY_FAILED`
111-
* `BATCH_PARTIAL_FAILURE`
112-
* `BATCH_COMPLETE_FAILURE`
113-
* `TRANSACTION_FAILED`
114111

115112

116113
---
@@ -294,9 +291,6 @@ const result = EnhancedApiErrorSchema.parse(data);
294291
* `EXTERNAL_SERVICE_ERROR`
295292
* `INTEGRATION_ERROR`
296293
* `WEBHOOK_DELIVERY_FAILED`
297-
* `BATCH_PARTIAL_FAILURE`
298-
* `BATCH_COMPLETE_FAILURE`
299-
* `TRANSACTION_FAILED`
300294

301295

302296
---

docs/adr/0112-error-code-vocabulary-and-ledger.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ Nine rulings, D1–D9.
103103

104104
**D2 — `StandardErrorCode` members are renamed in place (breaking).** Same 51 concepts, SCREAMING spelling (`permission_denied``PERMISSION_DENIED`). The enum remains the small, closed **standard catalog**: codes with platform-wide HTTP semantics (the `ErrorCategory`/status mapping). It does not try to swallow service-specific codes. `@objectstack/client` re-exports the type (`client/src/index.ts:12`) and uses it in `StandardError` (`:243`) — the client updates in the same PR.
105105

106+
> **Amendment (2026-08-18, [#9266](https://github.com/objectstack-ai/objectstack/issues/9266)) — the three batch-operation members are retired: `BATCH_PARTIAL_FAILURE`, `BATCH_COMPLETE_FAILURE`, `TRANSACTION_FAILED` leave the standard catalog.** The catalog's warrant (D2) is "codes with platform-wide HTTP semantics"; these three never earned it. Measured on 2026-08-18 (#9266's dev report is the evidence of record): no producer has ever emitted any of them — outside the enum declaration the only occurrences in the whole repo were two spec tests using them as arbitrary fixture strings, and `git log -S` over `packages/` shows they have never had a producer since this ADR introduced the vocabulary. The batch write path reports exactly these conditions with **more** information than an envelope code could carry: a rolled-back atomic batch marks each row `errors[0].code = ROLLED_BACK`, rows the abort never reached `NOT_ATTEMPTED`, and the causal row keeps its own error — both codes ledger-registered (D3), both answered at HTTP 200, which also means no doc-publishable HTTP status ever existed for the three (the `check:error-status-conformance` census that found them ungraded is what filed the card).
107+
>
108+
> **Ruled (maintainer, 2026-08-18): 「9266 同意 A」** — ADR-0049 enforce-or-remove applied to the error vocabulary itself. A catalog member no producer can speak is a declared-but-unenforced surface in the one contract every consumer branches on: it teaches an AI author to write `if (error.code === 'BATCH_PARTIAL_FAILURE')`, a branch that can never fire. After removal that spelling fails `ApiErrorSchema` parse at the vocabulary boundary — loud at authoring time, the posture this ADR exists to defend. Accept-set narrowing is acknowledged: a previously-parseable code becomes a parse refusal; the ADR-0087 notification channel is the `standard-error-code-batch-members-retired` semantic entry (no stored-metadata rewrite exists — error codes live on the wire, not in `sys_metadata`). The count claims elsewhere in this ADR ("51 concepts", "53") are historical readings at their own dates and are deliberately not rewritten.
109+
106110
**D3 — Service extension codes are registered, not free-typed.** Codes like `ATTACHMENT_DOWNLOAD_DENIED` or `UPLOAD_SESSION_NOT_FOUND` do not enter the standard catalog. Each service registers its codes in an **error-code ledger** (ADR-0060 pattern; same generation machinery as `api-surface.json` / `json-schema.manifest.json`). Registration carries the code, the owning service, and a one-line meaning. A recommended (not required) convention is a domain prefix (`ATTACHMENT_*`, `UPLOAD_*`). The ledger is the anti-bottleneck: adding a service code touches the service's own ledger entry, not the spec enum — but it is still a *deliberate, reviewable* act, which is the entropy gate this whole problem lacked.
107111

108112
**D4 — `ApiErrorSchema.code` stops being `z.string()`.** It becomes the generated union of the standard catalog and the registered ledger (`ErrorCode`, generated at build time — Zod-first per PD#1, enum generated from ledger + `StandardErrorCode`). Conformance suites thereby assert values for free. There is **no regex escape hatch** (see Alternatives — a casing regex passes hallucinated codes, which is precisely the AI failure mode this ADR exists to prevent).

packages/spec/src/api/batch.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -323,13 +323,13 @@ describe('BatchUpdateResponseSchema', () => {
323323
},
324324
],
325325
error: {
326-
code: 'BATCH_PARTIAL_FAILURE',
326+
code: 'INTERNAL_ERROR',
327327
message: 'Batch operation failed',
328328
},
329329
});
330330

331331
expect(response.failed).toBe(1);
332-
expect(response.error?.code).toBe('BATCH_PARTIAL_FAILURE');
332+
expect(response.error?.code).toBe('INTERNAL_ERROR');
333333
});
334334
});
335335

packages/spec/src/api/errors.test.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,14 @@ describe('StandardErrorCode', () => {
4242
expect(StandardErrorCode.parse('INSUFFICIENT_PRIVILEGES')).toBe('INSUFFICIENT_PRIVILEGES');
4343
});
4444

45-
it('should accept batch operation error codes', () => {
46-
expect(StandardErrorCode.parse('BATCH_PARTIAL_FAILURE')).toBe('BATCH_PARTIAL_FAILURE');
47-
expect(StandardErrorCode.parse('TRANSACTION_FAILED')).toBe('TRANSACTION_FAILED');
45+
it('refuses the retired batch-operation codes (ADR-0112 amendment 2026-08-18, #9266)', () => {
46+
// Retired under ADR-0049 enforce-or-remove: no producer ever emitted them;
47+
// the batch surface reports these conditions per row via the ledger-registered
48+
// ROLLED_BACK / NOT_ATTEMPTED codes instead. The wrong spelling must fail at
49+
// the vocabulary boundary rather than compile into a branch that never fires.
50+
expect(StandardErrorCode.safeParse('BATCH_PARTIAL_FAILURE').success).toBe(false);
51+
expect(StandardErrorCode.safeParse('BATCH_COMPLETE_FAILURE').success).toBe(false);
52+
expect(StandardErrorCode.safeParse('TRANSACTION_FAILED').success).toBe(false);
4853
});
4954
});
5055

packages/spec/src/api/errors.zod.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,12 @@ export const StandardErrorCode = z.enum([
117117
'EXTERNAL_SERVICE_ERROR', // External API call failed
118118
'INTEGRATION_ERROR', // Integration service error
119119
'WEBHOOK_DELIVERY_FAILED', // Webhook delivery failed
120-
121-
// Batch Operation Errors
122-
'BATCH_PARTIAL_FAILURE', // Batch operation partially succeeded
123-
'BATCH_COMPLETE_FAILURE', // Batch operation completely failed
124-
'TRANSACTION_FAILED', // Transaction rolled back
125120
]);
121+
// Retired (ADR-0112 amendment 2026-08-18, ADR-0049 enforce-or-remove, #9266):
122+
// BATCH_PARTIAL_FAILURE / BATCH_COMPLETE_FAILURE / TRANSACTION_FAILED — never
123+
// emitted by any producer in the repo's history; the batch surface reports these
124+
// conditions per row via the ledger-registered ROLLED_BACK / NOT_ATTEMPTED at
125+
// HTTP 200 instead of an envelope-level code.
126126

127127
export type StandardErrorCode = z.input<typeof StandardErrorCode>;
128128

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
import type { SemanticMigration } from '../../types.js';
4+
5+
export const entry: SemanticMigration = {
6+
id: 'standard-error-code-batch-members-retired',
7+
surface:
8+
'`error.code` values `BATCH_PARTIAL_FAILURE`, `BATCH_COMPLETE_FAILURE` and '
9+
+ '`TRANSACTION_FAILED` — three `StandardErrorCode` members retired from the closed '
10+
+ 'catalog (ADR-0112 amendment 2026-08-18), so constructing or parsing an ApiError '
11+
+ 'with any of them now refuses at the vocabulary boundary',
12+
replacement:
13+
'branch on the codes the batch surface actually speaks: a rolled-back atomic batch '
14+
+ 'marks each row `errors[0].code = ROLLED_BACK`, rows the abort never reached '
15+
+ '`NOT_ATTEMPTED`, and the causal row keeps its own error — all per row, at HTTP '
16+
+ '200, both codes ledger-registered. Delete any branch on the three retired '
17+
+ 'spellings outright: it never fired, because nothing ever emitted them',
18+
reason:
19+
'ADR-0049 enforce-or-remove applied to the error vocabulary. No producer has ever '
20+
+ 'emitted any of the three — measured on #9266: outside the enum declaration the '
21+
+ 'only occurrences in the whole repo were two spec tests using them as arbitrary '
22+
+ 'fixture strings, and `git log -S` shows they never had a producer since ADR-0112 '
23+
+ 'introduced the vocabulary. A catalog member no producer can speak teaches an AI '
24+
+ 'author a branch that can never fire; after removal the wrong spelling fails '
25+
+ 'parse at authoring time instead. This is a WIRE vocabulary, not stored metadata '
26+
+ '— no `sys_metadata` row exists for the D2 chain to rewrite, so (like '
27+
+ '`driver-sql-upsert-cross-row-identity-merge-refused`) this entry is the '
28+
+ 'notification channel. No mechanical rewrite exists: a dead branch has no '
29+
+ 'correct mechanical target — the per-row codes carry strictly more information '
30+
+ 'than the envelope code the branch expected. Maintainer ruling 2026-08-18: '
31+
+ '「9266 同意 A」. #9266, ADR-0112, ADR-0049.',
32+
acceptanceCriteria:
33+
'No consumer branches on the three retired spellings; batch failure handling reads '
34+
+ 'the per-row `results[].errors[].code` (`ROLLED_BACK` / `NOT_ATTEMPTED`) instead '
35+
+ 'of an envelope-level code; constructing an ApiError with a retired spelling '
36+
+ 'fails `StandardErrorCode`/`ApiErrorSchema` parse rather than passing silently.',
37+
};

0 commit comments

Comments
 (0)