Skip to content
Draft
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
21 changes: 21 additions & 0 deletions .changeset/driver-connect-summary-error-code-constants.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
"@objectstack/objectql": minor
---

Three more engine refusals publish their error `code` as an importable constant.

Each of these classes already tells the reader, in its own docblock, that it is *"Identified by `code` rather than `instanceof` so it survives crossing package boundaries"* — and none 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.

Three new exports from `@objectstack/objectql`, each graded on its own:

- `DRIVER_CONNECT_CODE` — `DriverConnectError`'s ADR-0112 `code`. Thrown by `ObjectQL.init()` when boot-registered drivers fail to connect, which aborts kernel bootstrap. **Additive widening, `minor`.**
- `DATASOURCE_UNAVAILABLE_CODE` — `DatasourceUnavailableError`'s ADR-0112 `code`. Thrown by `getDriver()` when an object's datasource was declared but has no live driver. **Additive widening, `minor`.**
- `SUMMARY_RECOMPUTE_CODE` — `SummaryRecomputeError`'s ADR-0112 `code`. Thrown by `insert`/`update`/`delete` when parent roll-up summaries fail to recompute *after the triggering records were written*. **Additive widening, `minor`.**

**The cost these close is already shipped, not hypothetical.** Three first-party packages in this repo match these refusals by `code` today and therefore carry a second spelling of the string: `packages/rest/src/error-response.ts` (datasource-unavailable), `packages/rest/src/import-runner.ts` and `packages/metadata-protocol/src/seed-loader.ts` (summary-recompute — both to implement the documented "the records WERE written, treat it as a warning" recovery). They keep working unchanged; they can now import the constant instead of authoring the string.

**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 these docblocks have 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.

**All three classes were already exported and stay exported.** The constants join them on the batteries barrel; like every other `*_CODE` in this package they are deliberately not added to the lean `core.ts` entry, even though `DriverConnectError` and `DatasourceUnavailableError` themselves are published there. That asymmetry is #16260's subject for the whole family and is not decided here.
82 changes: 82 additions & 0 deletions packages/objectql/src/datasource-unavailable-code-constant.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* #16159 row 2 of 3 in this batch — `DatasourceUnavailableError` publishes its
* ADR-0112 `code` as an importable constant.
*
* ## Why this row has the strongest evidence in the batch
*
* It has a LIVE first-party consumer doing precisely what the card describes:
* `packages/rest/src/error-response.ts` matches this refusal by `code` and then
* re-authors the same spelling into the response envelope it builds. That is
* two spellings of one refusal, in two packages, kept equal by nothing but a
* grep — and it is the ONLY option a consumer had, because the code was an
* inline literal with nothing to import.
*
* ⚠️ This refusal carries NO `status` field of its own (the REST door assigns
* the HTTP status when it recognises the code), so ADR-0112's `code` + `status`
* minimum reduces here to `code` plus the fields that discriminate the refusal.
* ⛔ Inventing a `status` on the class would be new published surface, which is
* not what this card converts.
*
* Five facts, each its own case. The reasoning behind each is spelled out in
* `driver-connect-code-constant.test.ts` (same batch, same shape); 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 BOTH `kind`s — `blocked` and `failed`. The MESSAGE branches
* on `kind` by design while the CODE deliberately does not; pinning both is
* what stops a future message split taking the code with it.
*/

import { describe, it, expect } from 'vitest';
import {
DatasourceUnavailableError,
DATASOURCE_UNAVAILABLE_CODE,
} from './driver-connect-errors.js';
import * as barrel from './index.js';

describe('#16159 DatasourceUnavailableError publishes its code as a constant', () => {
it('the constant holds the exact wire string it replaced', () => {
expect(DATASOURCE_UNAVAILABLE_CODE).toBe('ERR_DATASOURCE_UNAVAILABLE');
});

it('the constant IS the code both kinds of refusal carry — the code does not branch on kind', () => {
const blocked = new DatasourceUnavailableError('billing', 'crm_invoice', 'blocked');
const failed = new DatasourceUnavailableError('billing', 'crm_invoice', 'failed');

expect(blocked.code).toBe(DATASOURCE_UNAVAILABLE_CODE);
expect(failed.code).toBe(DATASOURCE_UNAVAILABLE_CODE);
expect(blocked.name).toBe('DatasourceUnavailableError');
expect(blocked.datasource).toBe('billing');
expect(blocked.objectName).toBe('crm_invoice');

// The MESSAGE branches on `kind` by design; the CODE deliberately does not.
expect(blocked.message).not.toBe(failed.message);
});

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.DATASOURCE_UNAVAILABLE_CODE).toBe(DATASOURCE_UNAVAILABLE_CODE);
});

it("the barrel's constant and the barrel's already-exported class name the same refusal", () => {
const err = new barrel.DatasourceUnavailableError('billing', 'crm_invoice', 'failed');
expect(err.code).toBe(barrel.DATASOURCE_UNAVAILABLE_CODE);
});

it("a `code` compare matches the OTHER realm's copy — the exact case `instanceof` gets wrong", () => {
class DatasourceUnavailableErrorOtherRealmCopy extends Error {
readonly code = 'ERR_DATASOURCE_UNAVAILABLE';
}
const fromOtherRealm = new DatasourceUnavailableErrorOtherRealmCopy();

// THE CONTROL — see the header.
expect(fromOtherRealm instanceof DatasourceUnavailableError).toBe(false);
expect(fromOtherRealm.code).toBe(DATASOURCE_UNAVAILABLE_CODE);
});
});
103 changes: 103 additions & 0 deletions packages/objectql/src/driver-connect-code-constant.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.

/**
* #16159 row 1 of 3 in this batch — `DriverConnectError` publishes its ADR-0112
* `code` as an importable constant.
*
* ## What this pins, and why each assertion is here
*
* `DriverConnectError`'s own docblock says it is "Identified by `code` rather
* than `instanceof` so it survives crossing package boundaries", 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` 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. Slice 1 (#16259) measured
* that mutating a constant's VALUE turns ONLY a case of this shape red,
* because every other case compares AGAINST the constant. ⛔ Do not
* "simplify" it into a constant compare; a pin that reads the constant
* cannot catch the constant being wrong.
* 2. the constant IS the code a real refusal carries, asserted with `name`
* and with the failure detail the class exists to report. ⛔ Never a bare
* `toThrow()`: this package's own history is the argument — 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 here, 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 docblock tells readers NOT to use.
*/

import { describe, it, expect } from 'vitest';
import { DriverConnectError, DRIVER_CONNECT_CODE } from './driver-connect-errors.js';
import * as barrel from './index.js';

describe('#16159 DriverConnectError publishes its code as a constant', () => {
it('the constant holds the exact wire string it replaced', () => {
expect(DRIVER_CONNECT_CODE).toBe('ERR_DRIVER_CONNECT');
});

it('the constant IS the code a boot-abort refusal carries', () => {
const err = new DriverConnectError(
[{ driverName: 'default', error: new Error('ECONNREFUSED 127.0.0.1:5432') }],
2,
);
expect(err.code).toBe(DRIVER_CONNECT_CODE);
expect(err.name).toBe('DriverConnectError');
// The refusal's payload is part of what a `code` match buys a caller: it
// names every driver that failed, which is why the CLI can print
// `error.message` alone.
expect(err.failedDrivers).toEqual(['default']);
expect(err.message).toContain('1 of 2 data driver(s) failed to connect');
});

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.DRIVER_CONNECT_CODE).toBe(DRIVER_CONNECT_CODE);
});

it("the barrel's constant and the barrel's already-exported class name the same refusal", () => {
const err = new barrel.DriverConnectError([{ driverName: 'reporting', error: 'timeout' }], 1);
expect(err.code).toBe(barrel.DRIVER_CONNECT_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 DriverConnectErrorOtherRealmCopy extends Error {
readonly code = 'ERR_DRIVER_CONNECT';
}
const fromOtherRealm = new DriverConnectErrorOtherRealmCopy();

// 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 DriverConnectError).toBe(false);
expect(fromOtherRealm.code).toBe(DRIVER_CONNECT_CODE);
});
});
73 changes: 71 additions & 2 deletions packages/objectql/src/driver-connect-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,49 @@ function failureMessage(error: unknown): string {
return String(error);
}

/**
* [#16159] The ADR-0112 `code` {@link DriverConnectError} carries, as a
* constant a consumer can import instead of re-spelling.
*
* The docblock below already says this refusal is "Identified by `code` rather
* than `instanceof` so it survives crossing package boundaries" — and until now
* offered nothing to import, so the only way to FOLLOW that instruction was to
* re-author the wire string in the consumer's own package. That 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 guidance and
* the surface disagreed; this closes that half of #16159's table.
*
* ⛔ 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_DRIVER_CONNECT` 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 = DRIVER_CONNECT_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` (an
* `as const` suffix on the FIELD would take it out of that pattern). ⛔ 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 (`HOOK_TARGET_REBIND_ERROR_CODE`,
* `READONLY_FIELD_REJECTED_CODE`). Re-exported from the `index.ts` barrel and,
* like every other `*_CODE` here, NOT from the lean `core.ts` entry — even
* though `DriverConnectError` itself IS on `core.ts`. That asymmetry is real,
* it is #16260's subject for the whole family, and ⛔ this mechanical sweep does
* not decide it.
*/
export const DRIVER_CONNECT_CODE = 'ERR_DRIVER_CONNECT' as const;

/**
* Thrown by `ObjectQL.init()` when one or more boot-registered drivers fail to
* connect (framework#3741). Aborts kernel bootstrap: a server that cannot reach
Expand All @@ -96,7 +139,7 @@ function failureMessage(error: unknown): string {
* boundaries.
*/
export class DriverConnectError extends Error {
readonly code = 'ERR_DRIVER_CONNECT' as const;
readonly code = DRIVER_CONNECT_CODE;

/**
* The first failure's `Error`, so its stack stays reachable for `DEBUG`
Expand Down Expand Up @@ -145,6 +188,32 @@ export interface DatasourceUnavailableInfo {
publicDetail?: string;
}

/**
* [#16159] The ADR-0112 `code` {@link DatasourceUnavailableError} carries, as a
* constant a consumer can import instead of re-spelling.
*
* This row has a live first-party consumer making the card's argument for it:
* `packages/rest/src/error-response.ts` matches this refusal by `code` and then
* re-authors the same spelling into the response envelope it builds — two
* spellings of one refusal, in two packages, with nothing but a grep keeping
* them equal.
*
* ⛔ The string is byte-identical to the literal it replaces — the conversion
* moves where a spelling lives, never what it says.
*
* ⚠️ `ERR_DATASOURCE_UNAVAILABLE` is registered in `ERROR_CODE_LEDGER` under
* BOTH `@objectstack/objectql` and the datasource-service owner key (it is one
* refusal raised from two sides), so this declaration is a `constdef` stamp
* site `check:error-code-provenance` sees and accepts under this package's own
* key, while `check:dispatcher-error-vocabulary` stays blind to it by
* construction. See the note on {@link DRIVER_CONNECT_CODE} for why those two
* gates answer oppositely.
*
* Naming, field spelling and barrel placement follow {@link DRIVER_CONNECT_CODE}
* exactly, including the `core.ts` asymmetry #16260 owns.
*/
export const DATASOURCE_UNAVAILABLE_CODE = 'ERR_DATASOURCE_UNAVAILABLE' as const;

/**
* Thrown by `getDriver()` when an object's `datasource` was **declared** but has
* no live driver, and the connection layer knows why (framework#3828).
Expand All @@ -171,7 +240,7 @@ export interface DatasourceUnavailableInfo {
* specific sets `publicReason` on its connect decision.
*/
export class DatasourceUnavailableError extends Error {
readonly code = 'ERR_DATASOURCE_UNAVAILABLE' as const;
readonly code = DATASOURCE_UNAVAILABLE_CODE;

constructor(
public readonly datasource: string,
Expand Down
Loading
Loading