Skip to content
Merged
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
18 changes: 18 additions & 0 deletions .changeset/objectql-system-write-organization-recognizer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@objectstack/objectql": minor
---

`@objectstack/objectql` now publishes a recognizer for the org-less system-write refusal, so a consumer no longer has to choose between an unsound check and a re-spelled string.

`SystemWriteOrganizationRequiredError` has always documented that it is identified by `code` rather than `instanceof`, "so the check survives crossing a package boundary where two copies of this module can exist". The convention was correct; the affordance for following it was missing. This package declares **both** realms in its own `exports` — `import` to `dist/index.mjs`, `require` to `dist/index.js` — so a consumer that loads it through the other realm than the engine did holds a second copy of the module. Measured across that split from a real consumer package: same class identity (`A === B`) **false**, `instA instanceof A` within one realm **true**, `instA instanceof B` across the two **false**, and a `code` compare **true**. So `instanceof` against this class was unsound for every consumer, and it failed silently — a `catch` that simply never fires.

That left a consumer with one sound option: re-spelling `'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED'` as a literal. That spelling is what `check:error-code-provenance` counts as a stamp site, so recognising one engine refusal cost the consumer's package a provenance decision of its own, and left the string spelled in two places with the typo failure mode standing — a typo in a `catch` produces a branch that never fires rather than an error.

Two new exports close it, both from the package root:

- **`SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE`** — the code as a value. Same shape as this package's five existing published codes (`DUPLICATE_RECORD_CODE`, `HOOK_TARGET_REBIND_ERROR_CODE`, `HOOK_UNSCOPED_DATA_ACCESS_CODE`, `MULTI_UPDATE_HOOK_KEY_DIVERGENCE_CODE`, `EMPTY_CREDENTIAL_REFUSAL_CODE`) rather than a new abstraction. The class field now reads from it, so exactly one spelling of the string remains in the package and a typo at an import site is a compile error instead of a dead branch.
- **`isSystemWriteOrganizationRequiredError(err): boolean`** — the code compare itself, so a consumer performs the sound check without authoring the string at all.

The predicate deliberately returns `boolean` and does **not** narrow to `err is SystemWriteOrganizationRequiredError`. A `code` compare is satisfied by any value carrying that code, including an envelope a transport rebuilt from the wire — #5437 withholds the prose and keeps the machine-readable code — so a type guard would promise `object`, `posture` and `reason` members such a value need not have, moving the unsoundness one layer down instead of removing it.

⛔ Nothing about the refusal itself changes: not its `code`, not its 500 status, not when it fires, and not the #8844 `derive-or-refuse` ruling behind it. `SystemWriteOrganizationRequiredError['code']` stays the literal type it was, which is what the existing cross-package consumer types its own constant from. This is purely an addition to what the package publishes.
11 changes: 11 additions & 0 deletions packages/objectql/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,12 +372,23 @@ export type {
// #8686's ruling. The refusal class is exported because a caller that catches
// it identifies it by `code`, and the decision function because it is the
// ruling's five binding points as one pure, directly-testable verdict.
//
// [#14936] `SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE` and
// `isSystemWriteOrganizationRequiredError` are the AFFORDANCE that makes "by
// `code`" followable without re-spelling the literal. Exporting the class was
// never enough on its own: this package declares both realms in its `exports`,
// so a consumer holding the other realm's copy gets `instanceof` === false,
// silently. The code compare is the check that survives; these two are how a
// consumer performs it without authoring the string itself, and so without
// acquiring a `check:error-code-provenance` stamp site of its own.
export {
resolveSystemWriteOrganization,
resolveTenantFieldName,
isPlatformNamespaceObject,
carriesOrganization,
isSystemWriteOrganizationRequiredError,
SystemWriteOrganizationRequiredError,
SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE,
ORGANIZATION_OBJECT,
GLOBAL_TENANT,
DEFAULT_TENANT_FIELD,
Expand Down
139 changes: 138 additions & 1 deletion packages/objectql/src/system-write-organization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@
import { describe, it, expect, vi, afterEach } from 'vitest';
import type { ExecutionContext } from '@objectstack/spec/kernel';
import { ObjectQL } from './engine.js';
import { resolveSystemWriteOrganization } from './tenancy/system-write-organization.js';
import {
resolveSystemWriteOrganization,
isSystemWriteOrganizationRequiredError,
SystemWriteOrganizationRequiredError,
SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE,
} from './tenancy/system-write-organization.js';

const ORG_ID = 'org_msokm9oaz0cal87q';
const SECOND_ORG_ID = 'org_second';
Expand Down Expand Up @@ -371,3 +376,135 @@ describe('#8844 the exclusions — populations the refusal must not touch', () =
expect(observed.filter((c) => c.object === 'sys_organization')).toEqual([]);
});
});


// ── [#14936] The published recognizer ────────────────────────────────────────
//
// The card's measurement, taken from a real consumer package loading
// `@objectstack/objectql` through each of the two realms its own `exports`
// declares (`import` -> `dist/index.mjs`, `require` -> `dist/index.js`):
//
// SAME CLASS IDENTITY (A === B): false
// instA instanceof A (same realm): true
// instA instanceof B (CROSS-REALM): false
// code compare survives the split: true
//
// So a consumer had exactly two options and both were bad: `instanceof`, which
// is unsound across that split and fails SILENTLY, or re-spelling
// `'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED'`, which the provenance gate counts
// as a stamp site in the consumer's own package and which can drift from what
// the engine throws. These pins cover the third option this card publishes.
//
// Each case below carries its DISCRIMINATING CONTROL, for the reason this
// file's header already states: a suite that only asserted "the recognizer
// says true" would stay green if the recognizer were `() => true`, and one
// that only asserted the same-realm instance would stay green if the
// recognizer were `instanceof`-based - which is the very defect being fixed.

/**
* What a SECOND copy of this module produces: structurally the refusal,
* nominally a different class. This is the CJS build's class arriving at a
* consumer holding the ESM one (or the reverse) - the exact shape the card's
* cross-realm measurement found, reproduced here without needing two builds.
*/
class SystemWriteOrganizationRequiredErrorOtherRealmCopy extends Error {
readonly code = 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED' as const;
readonly status = 500;
constructor() {
super('refused by the other realm\'s copy of this module');
this.name = 'SystemWriteOrganizationRequiredError';
}
}

describe('#14936 the published recognizer for the org-less system-write refusal', () => {
it('the published constant IS the code the thrown refusal carries, at its 500 status', () => {
const err = new SystemWriteOrganizationRequiredError('dispatch_order', 'isolated', 'walled-posture');
expect(err.code).toBe(SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE);
// ADR-0112 envelope: `code` AND `status`. Asserting the throw alone would
// stay green against an unrelated failure, and would not notice the status
// moving off 500 - which #8844 ruled deliberately.
expect(err.status).toBe(500);
// The wire string, spelled once here on purpose: this is the TEST layer,
// which `check:error-code-provenance` does not scan, so pinning it costs no
// stamp site while making a silent rename of the constant impossible to
// pass off as "still the same code".
expect(SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE).toBe('ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED');
});

it('recognises the refusal the engine actually throws', () => {
const err = new SystemWriteOrganizationRequiredError(
'dispatch_order', 'single', 'ambiguous-organization', 2,
);
expect(isSystemWriteOrganizationRequiredError(err)).toBe(true);
});

it("recognises the OTHER realm's copy - the exact case `instanceof` gets wrong", () => {
const fromOtherRealm = new SystemWriteOrganizationRequiredErrorOtherRealmCopy();
// THE CONTROL, and the whole point of the card. Without this line the
// assertion below would pass just as happily against an `instanceof`
// implementation, i.e. against the defect.
expect(fromOtherRealm instanceof SystemWriteOrganizationRequiredError).toBe(false);
expect(isSystemWriteOrganizationRequiredError(fromOtherRealm)).toBe(true);
});

it('recognises a transport-rebuilt envelope, which is WHY it does not narrow to the class', () => {
// #5437 withholds the prose from the wire and keeps the machine-readable
// `code`, so what a consumer catches downstream of a transport can be an
// envelope carrying the code and nothing else.
const wireEnvelope: Record<string, unknown> = {
code: 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED', status: 500,
};
expect(isSystemWriteOrganizationRequiredError(wireEnvelope)).toBe(true);
// ...and it carries none of the class's own members. A type guard
// (`err is SystemWriteOrganizationRequiredError`) would promise these,
// turning a sound check into an unsound assertion one layer down - which
// is why the predicate returns `boolean`.
expect(wireEnvelope.object).toBeUndefined();
expect(wireEnvelope.posture).toBeUndefined();
expect(wireEnvelope.reason).toBeUndefined();
});

it.each([
['null', null],
['undefined', undefined],
['a bare string carrying the code', 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED'],
['an Error with no code at all', new Error('boom')],
['a DIFFERENT engine refusal', Object.assign(new Error('dup'), { code: 'DUPLICATE_RECORD' })],
['a prefix lookalike', Object.assign(new Error('x'), { code: 'ERR_SYSTEM_WRITE_ORGANIZATION' })],
['a suffix lookalike', Object.assign(new Error('x'), { code: 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED_V2' })],
// DERIVED from the constant, never spelled. A lowercase literal in a `code`
// position is a real `check:error-code-casing` finding (ADR-0112 D1), and the
// gate cannot tell a negative fixture from a real emission - it classified this
// very site as `(emission)`. Deriving it is not an opt-out: the gate's own
// output says a code value with NO literal at the position is out of reach for
// its patterns BY CONSTRUCTION. It also makes the fixture track the constant
// instead of restating it - the same argument this card makes about consumers
// re-spelling literals, applied to its own test.
['the code in the wrong case', Object.assign(new Error('x'), {
code: SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE.toLowerCase(),
})],
])('refuses %s', (_label, value) => {
expect(isSystemWriteOrganizationRequiredError(value)).toBe(false);
});

it('keeps `code` a LITERAL type, which is what the cross-package consumer types itself from', () => {
// `plugin-sharing/src/sharing-rule-service.ts` declares its own constant as
// `SystemWriteOrganizationRequiredError['code']`. Had this refactor widened
// the class field to `string`, that consumer would keep COMPILING while
// silently losing the drift protection it asked for - so the widening is
// pinned as a TYPE error rather than a value assertion. This file carries no
// `test-typecheck-debt.json` entry, so a new error here is red on arrival.
const pinned: 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED' =
new SystemWriteOrganizationRequiredError('dispatch_order', 'isolated', 'walled-posture').code;
expect(pinned).toBe(SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE);
});

it('publishes both names from the package BARREL, not only from the module', async () => {
// The card's landing surface is "the module plus that package's index.ts
// export" - a consumer reaches these by bare specifier, so an export that
// exists only on the deep module is not the affordance that was asked for.
const barrel = await import('./index.js');
expect(barrel.SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE).toBe(SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE);
expect(barrel.isSystemWriteOrganizationRequiredError).toBe(isSystemWriteOrganizationRequiredError);
});
});
66 changes: 65 additions & 1 deletion packages/objectql/src/tenancy/system-write-organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,42 @@ function buildRefusalMessage(
);
}

/**
* The refusal's machine-readable code, published as a VALUE so a consumer can
* recognise the refusal without re-spelling the literal (#14936).
*
* The class below mandates `code` over `instanceof`, and the measurement
* behind that mandate is why this constant exists rather than staying implicit
* in the class field: `@objectstack/objectql` declares BOTH realms in its own
* `exports` (`import` -> `dist/index.mjs`, `require` -> `dist/index.js`), so a
* consumer that loads this package through the other realm than the engine did
* holds a SECOND copy of this module. Measured across that split from a real
* consumer package:
*
* SAME CLASS IDENTITY (A === B): false
* instA instanceof A (same realm): true
* instA instanceof B (CROSS-REALM): false
* code compare survives the split: true
*
* so `instanceof` against this class is unsound for every consumer and fails
* SILENTLY - a `catch` that simply never fires.
*
* Deliberately the same shape as this package's five other published codes
* (`DUPLICATE_RECORD_CODE`, `HOOK_TARGET_REBIND_ERROR_CODE`,
* `HOOK_UNSCOPED_DATA_ACCESS_CODE`, `MULTI_UPDATE_HOOK_KEY_DIVERGENCE_CODE`,
* `EMPTY_CREDENTIAL_REFUSAL_CODE`) rather than a new abstraction. The `*_CODE`
* NAME is load-bearing, not cosmetic: it is the shape
* `check:error-code-provenance`'s `constdef` pattern can see, so the one
* remaining spelling of this string is a stamp site the ledger accounts for
* under this package's own owner key - where
* `ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED` is already registered (#8844).
* ⛔ Never rename it out of that shape to quiet the gate: a spelling the
* gate cannot see is the failure mode the gate exists to catch, not a clean
* result.
*/
export const SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE =
'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED' as const;

/**
* Binding point 2's refusal.
*
Expand All @@ -326,7 +362,7 @@ function buildRefusalMessage(
* through the engine's own ERROR log.
*/
export class SystemWriteOrganizationRequiredError extends Error {
readonly code = 'ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED' as const;
readonly code = SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE;
readonly status = 500;

constructor(
Expand All @@ -339,3 +375,31 @@ export class SystemWriteOrganizationRequiredError extends Error {
this.name = 'SystemWriteOrganizationRequiredError';
}
}

/**
* Does `err` carry this module's refusal (#14936)?
*
* The recognizer a consumer should reach for instead of the two options it
* otherwise has: `instanceof`, which the measurement on
* {@link SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE} shows is unsound across the
* dual build, or a re-spelled string literal, which acquires a stamp site in
* the consumer's own package and can drift from what the engine throws. A
* `code` compare is the only one of the three that survives the realm split,
* and it is the convention this class's own docblock already mandates.
*
* ⛔ Deliberately returns `boolean` and does NOT narrow to
* `err is SystemWriteOrganizationRequiredError`. A `code` compare is satisfied
* by ANY value carrying that `code` - including an envelope a transport
* rebuilt from the wire, which keeps the machine-readable `code` and drops
* everything else - so a type guard would promise `object`, `posture` and
* `reason` members such a value need not have, turning a sound check into an
* unsound assertion one layer down. Read those fields off the caught value
* only after checking for them; `code` is what this predicate guarantees.
*/
export function isSystemWriteOrganizationRequiredError(err: unknown): boolean {
return (
typeof err === 'object'
&& err !== null
&& (err as { code?: unknown }).code === SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE
);
}
Loading