From 8c871e6373a9774b1a8c2013ec85624740a2bd59 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 13:34:18 +0000 Subject: [PATCH 1/4] feat(objectql): the two transaction-seam refusals publish their code as a constant `transaction-errors.ts` opens by telling the reader that the errors in it identify themselves by a `code` field rather than by `instanceof`, "for the reason `DriverConnectError` already records: the check has to survive crossing a package boundary, where two copies of this module can exist" -- and offered nothing to import. Following that published instruction meant re-spelling the wire string in the consumer's own package, which acquires a `check:error-code-provenance` stamp site there and is then free to drift from what this engine throws with no compile error to say so. `TRANSACTION_UNSUPPORTED_CODE` and `CROSS_DATASOURCE_TRANSACTION_WRITE_CODE` each hold text byte-identical to the literal they replace: the quoted wire spelling occurs exactly once per code in this file on both sides of the change, so it moved rather than multiplied. Both classes were already exported from the barrel and stay exported; the constants join them there. Neither class is published from the lean `./core` entry, so the batteries-only placement of these two constants adds nothing to #16260's population: class and constant are reachable from exactly the same entry point. Placement differs deliberately from the sibling batch on this card. Each constant and its docblock sit ABOVE the class's own docblock rather than between that docblock and the class. Measured with `tsc --declaration`: two consecutive JSDoc blocks are both emitted against the declaration that follows, so interposing the constant moves the class's documentation onto the CONSTANT in the published `.d.ts` and leaves the class with none. The grouped shape already landed in `registry.ts` avoids the same thing. Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ Co-authored-by: Claude --- packages/objectql/src/index.ts | 19 ++++- packages/objectql/src/transaction-errors.ts | 90 ++++++++++++++++++++- 2 files changed, 105 insertions(+), 4 deletions(-) diff --git a/packages/objectql/src/index.ts b/packages/objectql/src/index.ts index 0a62beebef..78a0f47126 100644 --- a/packages/objectql/src/index.ts +++ b/packages/objectql/src/index.ts @@ -219,12 +219,27 @@ export type { InsertManyRowOutcome } from './engine.js'; // [#5696] Thrown by `transaction(cb, base, { require: true })` when the // datasource cannot give a real transaction. Exported so a caller that fails // closed can narrow on the class; `code` is the boundary-crossing identity. -export { TransactionUnsupportedError } from './transaction-errors.js'; +// [#16159] `TRANSACTION_UNSUPPORTED_CODE` joins the class it names: the line +// above already tells a caller that `code` is the boundary-crossing identity, +// and offered nothing to import — so following it meant re-spelling the wire +// string in the consumer's own package. The class stays exported exactly as it +// was; this adds an affordance and removes nothing. +export { TransactionUnsupportedError, TRANSACTION_UNSUPPORTED_CODE } from './transaction-errors.js'; // [#5351/#5696] Thrown when a BUSINESS write inside an open transaction() // resolves to a driver that transaction does not cover. Append-only system // ledgers (lifecycle.class audit/telemetry/event) are carved out and never // raise it. Narrow on the class in-process; `code` crosses package boundaries. -export { CrossDatasourceTransactionWriteError } from './transaction-errors.js'; +// [#16159] `CROSS_DATASOURCE_TRANSACTION_WRITE_CODE` joins its class for the same +// reason (#14936: this package declares BOTH realms in its own `exports`, so a +// consumer holding the other realm's copy of the class gets `instanceof` === +// false, silently). ⭐ Neither class in `transaction-errors.js` is published from +// the lean `./core` entry, so batteries-only constants introduce no asymmetry +// here — class and constant are reachable from exactly one entry point, the same +// one. #16260 owns that question for the classes that ARE on `./core`. +export { + CrossDatasourceTransactionWriteError, + CROSS_DATASOURCE_TRANSACTION_WRITE_CODE, +} from './transaction-errors.js'; // [#4550] The delete-dispatch contract, exported so a TEST DOUBLE that stands // in for the engine can import the producer's own decision rather than diff --git a/packages/objectql/src/transaction-errors.ts b/packages/objectql/src/transaction-errors.ts index 2fc244be26..c93e7e0a03 100644 --- a/packages/objectql/src/transaction-errors.ts +++ b/packages/objectql/src/transaction-errors.ts @@ -10,6 +10,57 @@ * can exist. */ +/** + * [#16159] The ADR-0112 `code` {@link TransactionUnsupportedError} carries, as a + * constant a consumer can import instead of re-spelling. + * + * This module's own header already says these errors "identify themselves by a + * `code` field rather than by `instanceof`, for the reason `DriverConnectError` + * already records: the check has to survive crossing a package boundary, where + * two copies of this module can exist" — and until now offered nothing to + * import. The only way to FOLLOW that published instruction was to re-author + * the wire string in the consumer's own package, which acquires a + * `check:error-code-provenance` stamp site there and is then free to drift from + * what this engine throws, with no compile error to say so. + * + * ⛔ The string is byte-identical to the literal it replaces. This moves where a + * spelling lives, never what it says; renaming the code is a separate breaking + * decision and never a rider on this conversion. + * + * ⚠️ `ERR_TRANSACTION_UNSUPPORTED` IS registered in `ERROR_CODE_LEDGER` under + * `@objectstack/objectql`, so this declaration is a `constdef` stamp site + * `check:error-code-provenance` DOES see (that gate skips unregistered codes), + * and it is listed under this package's own owner key, which is what makes the + * gate accept it. Equally, no row moves in + * `packages/runtime/src/dispatcher-error-vocabulary.ts`: that table records + * UNREGISTERED code sites, so a registered code is invisible to it by + * construction. The two gates are exactly inverted — measured on this branch, + * not assumed. + * + * The `_CODE` NAME and the bare `readonly code = TRANSACTION_UNSUPPORTED_CODE;` + * spelling are load-bearing rather than cosmetic: the first is the shape + * `check:error-code-provenance`'s `constdef` pattern can see, the second is the + * shape `check:dispatcher-error-vocabulary` classifies as `classconst` — its + * pattern requires the constant name to be followed by `;`, `,` or a newline, + * so an `as const` suffix on the FIELD takes the site out of it. ⛔ Never rename + * out of either shape to quiet a gate. + * + * Dropping the `ERR_` prefix from the CONSTANT's name follows this package's + * existing precedents (`READONLY_FIELD_REJECTED_CODE`, + * `HOOK_TARGET_REBIND_ERROR_CODE`). Re-exported from the `index.ts` barrel, + * beside the class that is already published there. + * + * ⚠️ Placement is deliberate and differs from the sibling batch on this card: + * the constant and its docblock sit ABOVE the class's own docblock, not between + * that docblock and the class. Measured with `tsc --declaration`: two + * consecutive JSDoc blocks are both emitted against the declaration that + * follows them, so interposing this constant would move the class's + * documentation onto the CONSTANT in the published `.d.ts` and leave the class + * with none. That is a published-surface documentation regression, and the + * grouped shape #16259 landed in `registry.ts` already avoids it. + */ +export const TRANSACTION_UNSUPPORTED_CODE = 'ERR_TRANSACTION_UNSUPPORTED' as const; + /** * `transaction(cb, base, { require: true })` was called on a datasource whose * driver has no `beginTransaction` (#5696 point 1). @@ -23,7 +74,7 @@ * posture `batchData`'s atomic gate established (ADR-0119 D4). */ export class TransactionUnsupportedError extends Error { - readonly code = 'ERR_TRANSACTION_UNSUPPORTED' as const; + readonly code = TRANSACTION_UNSUPPORTED_CODE; constructor(public readonly datasource: string) { super( @@ -37,6 +88,41 @@ export class TransactionUnsupportedError extends Error { } } +/** + * [#16159] The ADR-0112 `code` {@link CrossDatasourceTransactionWriteError} + * carries, as a constant a consumer can import instead of re-spelling. + * + * This row is the one on the card whose refusal a caller is most likely to + * WANT to match rather than merely log: the class's docblock below says the + * decided behaviour is to "refuse, by name, before anything runs", and the + * remedy it prescribes ("split the work into per-datasource units and have the + * caller reconcile them explicitly") is a recovery a caller implements around + * this exact refusal. Matching it by `instanceof` is the thing #14936 measured + * as silently false across the realm split this package declares in its own + * `exports`; matching it by `code` meant re-spelling the string, until now. + * + * ⛔ The string is byte-identical to the literal it replaces — the conversion + * moves where a spelling lives, never what it says. + * + * ⚠️ `ERR_CROSS_DATASOURCE_TRANSACTION_WRITE` is registered in + * `ERROR_CODE_LEDGER` under `@objectstack/objectql`, so, exactly as for + * {@link TRANSACTION_UNSUPPORTED_CODE}, this declaration is a `constdef` stamp + * site `check:error-code-provenance` sees and accepts under this package's own + * owner key, while `check:dispatcher-error-vocabulary` — which records only + * UNREGISTERED sites — stays blind to it by construction. + * + * ⭐ Unlike the two `driver-connect-errors.ts` rows of this card's sweep, + * NEITHER class in this file is published from the lean `./core` entry, so the + * batteries-only placement of these constants introduces no asymmetry at all + * here: class and constant are reachable from exactly the same entry point. + * #16260 owns that question for the classes that ARE on `./core`; this file + * adds nothing to its population. + * + * Naming, field spelling and placement follow + * {@link TRANSACTION_UNSUPPORTED_CODE} exactly. + */ +export const CROSS_DATASOURCE_TRANSACTION_WRITE_CODE = 'ERR_CROSS_DATASOURCE_TRANSACTION_WRITE' as const; + /** * A BUSINESS write inside an open `transaction()` resolved to a driver that * transaction does not cover (#5696 point 2, decided together with #5351 by the @@ -61,7 +147,7 @@ export class TransactionUnsupportedError extends Error { * `isSystemLedgerObject` in the engine. */ export class CrossDatasourceTransactionWriteError extends Error { - readonly code = 'ERR_CROSS_DATASOURCE_TRANSACTION_WRITE' as const; + readonly code = CROSS_DATASOURCE_TRANSACTION_WRITE_CODE; constructor( public readonly object: string, From 4c2c2eeb94d65db07d4947c0b559e4b80c387ec3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 13:34:37 +0000 Subject: [PATCH 2/4] test(objectql): pin TRANSACTION_UNSUPPORTED_CODE against its class and the barrel Row 1 of this batch, graded and pinned on its own. Five cases: the literal wire string (the byte-identity fence -- deliberately NOT a constant compare, because a pin that reads the constant cannot catch the constant being wrong); the code a real refusal carries, asserted with `name` and the datasource it reports rather than a bare `toThrow()`; barrel reachability by identity; barrel-class and barrel-constant agreement; and a cross-realm copy where `instanceof` is false while the `code` compare holds -- the control without which the file would pass just as happily against the `instanceof` recommendation this module tells readers not to use. This refusal carries no `status` field, so ADR-0112's `code` + `status` minimum reduces here to `code` plus the fields that discriminate it; inventing a `status` would be new published surface and is not what this card converts. Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ Co-authored-by: Claude --- ...nsaction-unsupported-code-constant.test.ts | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 packages/objectql/src/transaction-unsupported-code-constant.test.ts diff --git a/packages/objectql/src/transaction-unsupported-code-constant.test.ts b/packages/objectql/src/transaction-unsupported-code-constant.test.ts new file mode 100644 index 0000000000..b1f870a68f --- /dev/null +++ b/packages/objectql/src/transaction-unsupported-code-constant.test.ts @@ -0,0 +1,103 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * #16159 row 1 of 2 in this batch — `TransactionUnsupportedError` publishes its + * ADR-0112 `code` as an importable constant. + * + * ## What this pins, and why each assertion is here + * + * `transaction-errors.ts`'s module header already says the errors in it + * "identify themselves by a `code` field rather than by `instanceof`, for the + * reason `DriverConnectError` already records: the check has to survive + * crossing a package boundary, where two copies of this module can exist" — and + * until this change offered nothing to import. Following that published + * instruction meant RE-SPELLING the wire string in the consumer's own package, + * which acquires a `check:error-code-provenance` stamp site there and can then + * drift from what this engine throws with no compile error to say so. + * + * ⚠️ This refusal carries NO `status` field, so ADR-0112's `code` + `status` + * minimum reduces here to `code` plus the fields that discriminate the refusal. + * ⛔ Inventing a `status` on the class to satisfy a habit would be new published + * surface, and that is not what this card converts. + * + * Five facts, each its own case so a failure reads as the specific regression: + * + * 1. the constant holds the exact wire string, spelled LITERALLY here on + * purpose. The test layer sits outside `check:error-code-provenance`'s + * scanned population, so pinning it costs no stamp site while making a + * silent rename of a published code impossible to pass off as "still the + * same code". ⛔ This is the byte-identity fence — the conversion moves + * where a spelling lives, never what it says. ⛔ Do not "simplify" it into + * a constant compare: a pin that reads the constant cannot catch the + * constant being wrong, and every OTHER case in this file compares against + * the constant, so this is the only case that can. + * 2. the constant IS the code a real refusal carries, asserted with `name` + * and with the field the class exists to report. ⛔ Never a bare + * `toThrow()`: a throw-shaped assertion stays green when a DIFFERENT + * refusal fires one step later, which is exactly the confusion `code` is + * meant to end. + * 3. it is reachable from the package BARREL, which is the whole affordance + * this card buys — a constant a consumer cannot import is not an answer to + * "identify it by `code`" — and it is what a future barrel edit would lose + * silently. + * 4. the barrel's constant and the barrel's already-exported class name the + * same refusal. Both routes are published, so a consumer can hold either + * and they must agree. + * 5. a `code` compare matches a foreign-realm copy of the refusal where + * `instanceof` returns false. THE CONTROL, and the reason the convention + * exists (#14936): `@objectstack/objectql` declares both realms in its own + * `exports`, so a consumer holding the other realm's copy of the class + * gets `instanceof` === false, silently. Without this case the others + * would pass just as happily against an `instanceof`-based recommendation + * — the thing the module header tells readers NOT to use. + */ + +import { describe, it, expect } from 'vitest'; +import { TransactionUnsupportedError, TRANSACTION_UNSUPPORTED_CODE } from './transaction-errors.js'; +import * as barrel from './index.js'; + +describe('#16159 TransactionUnsupportedError publishes its code as a constant', () => { + it('the constant holds the exact wire string it replaced', () => { + expect(TRANSACTION_UNSUPPORTED_CODE).toBe('ERR_TRANSACTION_UNSUPPORTED'); + }); + + it('the constant IS the code the require:true refusal carries', () => { + const err = new TransactionUnsupportedError('reporting'); + + expect(err.code).toBe(TRANSACTION_UNSUPPORTED_CODE); + expect(err.name).toBe('TransactionUnsupportedError'); + // The datasource is what a caller acts on: it names which driver has to + // gain `beginTransaction` for this call to be honoured. + expect(err.datasource).toBe('reporting'); + // Refused BEFORE the callback runs — the fail-closed posture ADR-0119 D4 + // established, and the half of the message a caller is told to trust. + expect(err.message).toContain('nothing has been written'); + }); + + it('it is re-exported from the package barrel, which is where a consumer reaches it', () => { + // Identity, not equality: a barrel that re-declared the string instead of + // re-exporting the constant would satisfy `toBe` on the VALUE while having + // re-introduced exactly the second spelling this card exists to remove. + expect(barrel.TRANSACTION_UNSUPPORTED_CODE).toBe(TRANSACTION_UNSUPPORTED_CODE); + }); + + it("the barrel's constant and the barrel's already-exported class name the same refusal", () => { + const err = new barrel.TransactionUnsupportedError('billing'); + expect(err.code).toBe(barrel.TRANSACTION_UNSUPPORTED_CODE); + }); + + it("a `code` compare matches the OTHER realm's copy — the exact case `instanceof` gets wrong", () => { + // What a consumer holding the other realm's copy of this module actually + // has: a structurally identical refusal from a DIFFERENT class object. + class TransactionUnsupportedErrorOtherRealmCopy extends Error { + readonly code = 'ERR_TRANSACTION_UNSUPPORTED'; + } + const fromOtherRealm = new TransactionUnsupportedErrorOtherRealmCopy(); + + // THE CONTROL. Without this line the assertion below would pass against an + // `instanceof` recommendation too, i.e. against the defect the convention + // exists to avoid. + expect(fromOtherRealm instanceof TransactionUnsupportedError).toBe(false); + expect(fromOtherRealm.code).toBe(TRANSACTION_UNSUPPORTED_CODE); + }); +}); From 29f099178335c0b9f8567e9ff8c7a6d983705af1 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 13:34:38 +0000 Subject: [PATCH 3/4] test(objectql): pin CROSS_DATASOURCE_TRANSACTION_WRITE_CODE against its class and the barrel Row 2 of this batch, graded and pinned on its own, in the shape row 1 spells out. Case 2 drives all three write operations: the message embeds the operation by design while the code deliberately does not, so the three messages differ and the three codes are equal -- which is what stops a future per-operation message split taking the code with it. It also pins the four fields a caller reads to apply the remedy the message prescribes (split the unit per datasource), since recognising the refusal is the step that recovery begins with. Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ Co-authored-by: Claude --- ...ce-transaction-write-code-constant.test.ts | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 packages/objectql/src/cross-datasource-transaction-write-code-constant.test.ts diff --git a/packages/objectql/src/cross-datasource-transaction-write-code-constant.test.ts b/packages/objectql/src/cross-datasource-transaction-write-code-constant.test.ts new file mode 100644 index 0000000000..d2f70768d4 --- /dev/null +++ b/packages/objectql/src/cross-datasource-transaction-write-code-constant.test.ts @@ -0,0 +1,106 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * #16159 row 2 of 2 in this batch — `CrossDatasourceTransactionWriteError` + * publishes its ADR-0112 `code` as an importable constant. + * + * ## Why this row is the batch's strongest case for the affordance + * + * This refusal is one a caller is meant to RECOVER from, not merely log: the + * class's own docblock records the decided behaviour as "refuse, by name, + * before anything runs", and the remedy it prescribes — "split the work into + * per-datasource units and have the caller reconcile them explicitly" — is + * something a caller implements AROUND this exact refusal. Recognising it by + * `instanceof` is what #14936 measured as silently false across the realm split + * this package declares in its own `exports`; recognising it by `code` meant + * re-authoring the wire string, until now. + * + * ⚠️ This refusal carries NO `status` field, so ADR-0112's `code` + `status` + * minimum reduces here to `code` plus the fields that discriminate the refusal. + * ⛔ Inventing a `status` would be new published surface, which is not what this + * card converts. + * + * The five facts and the reasoning behind each are spelled out in + * `transaction-unsupported-code-constant.test.ts` (same batch, same shape, + * same file). The load-bearing points repeated here are (1) the literal + * spelling is the byte-identity fence and must NOT be "simplified" into a + * constant compare, and (5) the cross-realm case is the control without which + * the whole file would pass just as happily against an `instanceof` + * recommendation. + * + * ⭐ Case 2 drives all three write operations. The MESSAGE embeds the operation + * by design while the CODE deliberately does not; pinning all three is what + * stops a future per-operation message split taking the code with it. + */ + +import { describe, it, expect } from 'vitest'; +import { + CrossDatasourceTransactionWriteError, + CROSS_DATASOURCE_TRANSACTION_WRITE_CODE, +} from './transaction-errors.js'; +import * as barrel from './index.js'; + +describe('#16159 CrossDatasourceTransactionWriteError publishes its code as a constant', () => { + it('the constant holds the exact wire string it replaced', () => { + expect(CROSS_DATASOURCE_TRANSACTION_WRITE_CODE).toBe('ERR_CROSS_DATASOURCE_TRANSACTION_WRITE'); + }); + + it('the constant IS the code every operation carries — the code does not branch on the verb', () => { + const operations = ['insert', 'update', 'delete'] as const; + const errors = operations.map( + (operation) => + new CrossDatasourceTransactionWriteError('crm_invoice', operation, 'billing', 'default'), + ); + + for (const err of errors) { + expect(err.code).toBe(CROSS_DATASOURCE_TRANSACTION_WRITE_CODE); + expect(err.name).toBe('CrossDatasourceTransactionWriteError'); + } + + // The four fields are what a `code` match buys a caller: which write, on + // which object, and the two datasources whose divergence caused the + // refusal. They are how a caller splits the unit per datasource, which is + // the remedy the message prescribes. + const [insertError] = errors; + expect(insertError.object).toBe('crm_invoice'); + expect(insertError.operation).toBe('insert'); + expect(insertError.datasource).toBe('billing'); + expect(insertError.transactionDatasource).toBe('default'); + + // The MESSAGE embeds the operation by design; the CODE deliberately does + // not, so the three messages differ while the three codes are equal. + expect(new Set(errors.map((e) => e.message)).size).toBe(3); + // Fail-closed, and the sentence a caller is told to trust. + expect(insertError.message).toContain('Nothing was written'); + }); + + it('it is re-exported from the package barrel, which is where a consumer reaches it', () => { + // Identity, not equality: a barrel that re-declared the string instead of + // re-exporting the constant would satisfy `toBe` on the VALUE while having + // re-introduced the second spelling this card exists to remove. + expect(barrel.CROSS_DATASOURCE_TRANSACTION_WRITE_CODE).toBe( + CROSS_DATASOURCE_TRANSACTION_WRITE_CODE, + ); + }); + + it("the barrel's constant and the barrel's already-exported class name the same refusal", () => { + const err = new barrel.CrossDatasourceTransactionWriteError( + 'crm_payment', + 'update', + 'ledger', + 'default', + ); + expect(err.code).toBe(barrel.CROSS_DATASOURCE_TRANSACTION_WRITE_CODE); + }); + + it("a `code` compare matches the OTHER realm's copy — the exact case `instanceof` gets wrong", () => { + class CrossDatasourceTransactionWriteErrorOtherRealmCopy extends Error { + readonly code = 'ERR_CROSS_DATASOURCE_TRANSACTION_WRITE'; + } + const fromOtherRealm = new CrossDatasourceTransactionWriteErrorOtherRealmCopy(); + + // THE CONTROL — see the header. + expect(fromOtherRealm instanceof CrossDatasourceTransactionWriteError).toBe(false); + expect(fromOtherRealm.code).toBe(CROSS_DATASOURCE_TRANSACTION_WRITE_CODE); + }); +}); From c4e60773b6c4f1751355b56346772f89e96979ea Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 13:34:38 +0000 Subject: [PATCH 4/4] chore(changeset): minor for the two transaction-seam code constants Both rows are additive widening of a published surface and nothing is removed, so each is graded `minor` on its own line rather than as one lump. The entry records what a consumer gains, why `code` and not `instanceof`, and that the wire strings are byte-identical to the literals they replace. Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ Co-authored-by: Claude --- .../transaction-error-code-constants.md | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .changeset/transaction-error-code-constants.md diff --git a/.changeset/transaction-error-code-constants.md b/.changeset/transaction-error-code-constants.md new file mode 100644 index 0000000000..8a5a054256 --- /dev/null +++ b/.changeset/transaction-error-code-constants.md @@ -0,0 +1,20 @@ +--- +"@objectstack/objectql": minor +--- + +Both transaction-seam refusals publish their error `code` as an importable constant. + +`packages/objectql/src/transaction-errors.ts` opens by telling the reader that the errors in it "identify themselves by a `code` field rather than by `instanceof`, for the reason `DriverConnectError` already records: the check has to survive crossing a package boundary, where two copies of this module can exist" — and neither of them offered anything to import. The only way to FOLLOW that published instruction was to re-spell the wire string in your own package, which acquires a `check:error-code-provenance` stamp site there and can then drift from what the engine throws with no compile error to say so. + +Two new exports from `@objectstack/objectql`, each graded on its own: + +- `TRANSACTION_UNSUPPORTED_CODE` — `TransactionUnsupportedError`'s ADR-0112 `code`. Thrown by `transaction(cb, base, { require: true })` when the datasource's driver has no `beginTransaction`, refused before the callback runs so nothing has been written. **Additive widening, `minor`.** +- `CROSS_DATASOURCE_TRANSACTION_WRITE_CODE` — `CrossDatasourceTransactionWriteError`'s ADR-0112 `code`. Thrown when a business write inside an open `transaction()` resolves to a driver that transaction does not cover. **Additive widening, `minor`.** + +**The second one is a refusal callers are meant to recover from.** Its own message prescribes the remedy — split the work into per-datasource units and reconcile them explicitly — which is code a caller writes *around* this refusal, and therefore code that has to recognise it first. That recognition now has something to import. + +**Why `code` and not `instanceof`.** This package declares both realms in its own `exports` (`import` reaches `dist/index.mjs`, `require` reaches `dist/index.js`), so a consumer holding the other realm's copy of a class gets `instanceof` === false — measured, and silent. A `code` compare is the check that survives that boundary, which is what this module's header has been telling readers to do. + +**Nothing about the wire changed.** Each constant holds text byte-identical to the literal it replaces; every refusal throws the same `code` and the same message as before. Consumers that spell the strings themselves keep working unchanged — this adds affordances, it removes nothing. + +**Both classes were already exported and stay exported**, and neither is published from the lean `./core` entry, so the constants join them on the one entry point that publishes them: class and constant are reachable from exactly the same place.