From 9a0052002362f1ec7e8ff3ad5ac1d30ed7b6a08b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 03:04:26 +0000 Subject: [PATCH 1/3] feat(objectql): publish SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE and isSystemWriteOrganizationRequiredError (#14936) Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ --- packages/objectql/src/index.ts | 11 ++++ .../src/tenancy/system-write-organization.ts | 66 ++++++++++++++++++- 2 files changed, 76 insertions(+), 1 deletion(-) diff --git a/packages/objectql/src/index.ts b/packages/objectql/src/index.ts index 8fc9bc1efc..6c11543865 100644 --- a/packages/objectql/src/index.ts +++ b/packages/objectql/src/index.ts @@ -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, diff --git a/packages/objectql/src/tenancy/system-write-organization.ts b/packages/objectql/src/tenancy/system-write-organization.ts index 345138037f..ece69aabb1 100644 --- a/packages/objectql/src/tenancy/system-write-organization.ts +++ b/packages/objectql/src/tenancy/system-write-organization.ts @@ -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. * @@ -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( @@ -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 + ); +} From 032f9d3b168c11a825f21f90d5f0fc331baf9a21 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 03:06:55 +0000 Subject: [PATCH 2/3] test(objectql): pin the recognizer across the realm split, with its discriminating controls (#14936) Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ --- ...ql-system-write-organization-recognizer.md | 18 +++ .../src/system-write-organization.test.ts | 129 +++++++++++++++++- 2 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 .changeset/objectql-system-write-organization-recognizer.md diff --git a/.changeset/objectql-system-write-organization-recognizer.md b/.changeset/objectql-system-write-organization-recognizer.md new file mode 100644 index 0000000000..74db9a4109 --- /dev/null +++ b/.changeset/objectql-system-write-organization-recognizer.md @@ -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. diff --git a/packages/objectql/src/system-write-organization.test.ts b/packages/objectql/src/system-write-organization.test.ts index 07abb3fcce..eb0b029ee8 100644 --- a/packages/objectql/src/system-write-organization.test.ts +++ b/packages/objectql/src/system-write-organization.test.ts @@ -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'; @@ -371,3 +376,125 @@ 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 = { + 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' })], + ['the code in the wrong case', Object.assign(new Error('x'), { code: 'err_system_write_organization_required' })], + ])('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); + }); +}); From a4255ac68e7f418f7c71b098376bd38a5b603c4d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 6 Sep 2026 04:04:22 +0000 Subject: [PATCH 3/3] test(objectql): derive the wrong-case fixture from the published constant instead of spelling it (#14936) A lowercase error-code literal in a `code` position is an ADR-0112 D1 finding, and `check:error-code-casing` cannot tell a negative fixture from a real emission - it classified this site as `(emission)`. Deriving the value from SYSTEM_WRITE_ORGANIZATION_REQUIRED_CODE is not an opt-out: the gate's own output records that a code value with no literal at the position is out of reach for its patterns by construction. No `adr0112-ok:` suppression was added (the count stays 17) and KNOWN_LOWERCASE_CODES is untouched. It also makes the fixture track the constant rather than restate it - this card's own argument about consumers re-spelling literals, applied to its own test. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ --- .../objectql/src/system-write-organization.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/objectql/src/system-write-organization.test.ts b/packages/objectql/src/system-write-organization.test.ts index eb0b029ee8..14250432d4 100644 --- a/packages/objectql/src/system-write-organization.test.ts +++ b/packages/objectql/src/system-write-organization.test.ts @@ -472,7 +472,17 @@ describe('#14936 the published recognizer for the org-less system-write refusal' ['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' })], - ['the code in the wrong case', Object.assign(new Error('x'), { code: 'err_system_write_organization_required' })], + // 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); });