Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
'@objectstack/core': patch
'@objectstack/driver-memory': patch
'@objectstack/driver-mongodb': patch
'@objectstack/formula': patch
'@objectstack/metadata': patch
'@objectstack/metadata-core': patch
'@objectstack/objectql': patch
'@objectstack/platform-objects': patch
---

Refusals, log lines and field help in core, the in-memory and MongoDB drivers, formula, metadata, metadata-core, objectql and platform-objects no longer cite tracker numbers; each states the reason in words

Clause-②: no

Many messages these packages show to authors, administrators and operators ended with an issue-tracker
number where the reason belonged. The number goes, and where the sentence did not already say what was
decided, it now does. Where an ADR stood beside the number, the ADR stays.

- Refusals and prescriptions: the retired health-check keys, the `IMetadataService.register` refusals
(the contract refuses loudly and names the mismatch, never coerces a value into storability), the
kernel's plugin-ordering errors (registration order is not a contract), the in-memory and MongoDB
filter and aggregation refusals, formula's empty field constraint, the retired `artifact-api`
source, and the by-id update and delete refusals. The MongoDB retired-aggregate refusal now says the
function left `AggregationFunction` because no SQL backend compiled it; its undeclared-aggregate
refusal says the builder used to sum an unrecognised name before this refusal existed.
- The `findOne` no-predicate refusal loses its citation in `objectql` and in `metadata-core`'s
`engineFindOnePredicateRefusalMessage` together, so the two still read byte for byte the same.
- The in-memory and MongoDB drivers' multi-tenancy refusals (`MEMORY_MULTI_TENANT_UNSUPPORTED`,
`MONGODB_MULTI_TENANT_UNSUPPORTED`) no longer end with a `Tracking:` line linking a tracker card;
the sentence above it already says the driver refuses rather than run or answer unisolated.
- Field help and protection text: the `sys_account` token help (and its es-ES, ja-JP and zh-CN
translations), the `sys_email` headers help and the SCIM credential store's protection reason.
- Log lines: the superseded-registration warning, the authz cache posture line, the endpoint matcher's
excluded-item error, the metadata history and loader-read failure errors, and the fresh-datastore
attestation info lines.

Text only: no error code, field name, status or behaviour changes.
2 changes: 1 addition & 1 deletion packages/core/src/health-monitor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -543,7 +543,7 @@ describe('PluginHealthMonitor', () => {

const message = caught?.message ?? '';
expect(message).toContain(`'${key}' was removed`);
expect(message).toContain('#12032');
expect(message).toContain('in @objectstack/spec 17.3.0 (ADR-0049 enforce-or-remove)');
expect(message).toContain('ADR-0049');
expect(message).toContain('Delete the key');
// The affordance that exists, named in place of the one that did not.
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/health-monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ const RETIRED_HEALTH_CHECK_KEYS: ReadonlyArray<readonly [string, string]> = [
[
'autoRestart',
"'autoRestart' was removed from PluginHealthCheck in @objectstack/spec 17.3.0 "
+ '(#12032, ADR-0049 enforce-or-remove) — it never restarted a plugin. '
+ '(ADR-0049 enforce-or-remove) — it never restarted a plugin. '
+ '`attemptRestart` called `plugin.destroy()` and stopped there, then '
+ "logged 'Plugin restarted' and set status `recovering`, and the periodic "
+ 'checks carried on against the destroyed instance — which the default '
Expand All @@ -94,14 +94,14 @@ const RETIRED_HEALTH_CHECK_KEYS: ReadonlyArray<readonly [string, string]> = [
[
'maxRestartAttempts',
"'maxRestartAttempts' was removed from PluginHealthCheck in "
+ '@objectstack/spec 17.3.0 (#12032, ADR-0049 enforce-or-remove) — it capped a '
+ '@objectstack/spec 17.3.0 (ADR-0049 enforce-or-remove) — it capped a '
+ 'restart that never happened, so it only counted `destroy()` calls. '
+ 'Delete the key.',
],
[
'restartBackoff',
"'restartBackoff' was removed from PluginHealthCheck in @objectstack/spec "
+ '17.3.0 (#12032, ADR-0049 enforce-or-remove) — it delayed a restart that '
+ '17.3.0 (ADR-0049 enforce-or-remove) — it delayed a restart that '
+ 'never happened, so it only moved when the `destroy()` landed. Delete '
+ 'the key.',
],
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/metadata-service-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export function assertMetadataRegisterContract(
throw registerRefusal(
`IMetadataService.register('${type}', '${name}'): data is ${shape}, not a metadata document. ` +
`register() stores plain-object documents only — accepting a value the service cannot key was measured as ` +
`accept-then-drop on document-keyed stores (#7378 row 3: refuse loudly, never coerce into storability). ` +
`accept-then-drop on document-keyed stores, so the contract refuses loudly and never coerces a value into storability. ` +
`Wrap the value in a document object whose shape the '${type}' type's schema accepts, or store it under a type that declares one.`,
);
}
Expand All @@ -169,7 +169,7 @@ export function assertMetadataRegisterContract(
throw registerRefusal(
`IMetadataService.register('${type}', '${name}'): data.name is '${String(documentName)}', which disagrees with the ` +
`name argument '${name}'. A disagreement is almost always an authoring bug, and resolving it silently in either ` +
`direction can file the item under a key the caller never wrote (#7378 row 1: refuse loudly, locate the mismatch). ` +
`direction can file the item under a key the caller never wrote, so the contract refuses loudly and names the mismatch. ` +
`Register under one name: pass the intended key as the argument and make data.name match it, or omit data.name.`,
);
}
Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/plugin-order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ export function validateInitServiceContract<P extends OrderablePlugin>(

if (violations.length > 0) {
throw new Error(
`[Kernel] Plugin ordering contract violated (#4131):\n - ${violations.join('\n - ')}`
`[Kernel] Plugin ordering contract violated:\n - ${violations.join('\n - ')}`
);
}
}
Expand All @@ -178,7 +178,7 @@ export function describeInitOrderFault(
}
}
return ` (while plugin '${currentlyInitializing}' was initializing — a composition/` +
`ordering fault, #4131.${providerHint})`;
`ordering fault; registration order is not a contract.${providerHint})`;
}

/**
Expand All @@ -200,7 +200,7 @@ export function assertInitServiceRequirements(
`service is registered at this point of the boot. No composed plugin that initializes ` +
`earlier provides it — compose a provider (and, if it initializes later without declaring ` +
`'${service}' in providesServices, order it ahead via this plugin's dependencies/` +
`optionalDependencies) (#4131).`
`optionalDependencies).`
);
}
}
2 changes: 1 addition & 1 deletion packages/core/src/plugin-registration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export function describeSupersededRegistration(
`REPLACED the earlier one (${versionLabel(previous)}). Only the later instance is ` +
`initialized and started; the earlier one is discarded without ever running init(). ` +
`Duplicate registration by name is last-one-wins on both kernels by declared contract ` +
`(#9864) — register the plugin once if that is not what you meant.`
`— register the plugin once if that is not what you meant.`
);
}

Expand Down
6 changes: 3 additions & 3 deletions packages/core/src/security/admin-standing-surface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ export const ADMIN_STANDING_SURFACE: Readonly<Record<string, AdminStandingTable>
reason:
'The row `admin_full_access` is resolved BY NAME from (§6b) — `platform_admin` is the '
+ "POSITION that row derives, not the row's own name. Renaming it, deleting it or switching "
+ 'it off (ADR-0049 `active`, read here since #8613) un-makes every GRANT-derived platform '
+ 'it off (ADR-0049 `active`, which this resolver honours) un-makes every GRANT-derived platform '
+ 'admin at once, with no identity table touched. ⚠️ It does NOT un-make a CONFIG-derived '
+ 'one (§6b-config, #11970): that route sets the same standing from '
+ 'one (§6b-config): that route sets the same standing from '
+ "`ADMIN_FULL_ACCESS_CAPABILITIES` in `@objectstack/spec` and matches the caller's own "
+ 'stored `sys_user` row, so it touches an identity table and never reads this one. With '
+ '`OS_PLATFORM_OWNER_EMAIL` unset the first sentence is the whole truth; with it declared, '
Expand Down Expand Up @@ -176,7 +176,7 @@ export const ADMIN_STANDING_SURFACE: Readonly<Record<string, AdminStandingTable>
sys_user: {
role: 'derives',
reason:
'[#11663 L2] RECLASSIFIED from `reads-only`. This table used to be read only for the '
'RECLASSIFIED from `reads-only` when the config anchor landed. This table used to be read only for the '
+ '`current_user.email` RLS fallback and the cloud ADR-0024 `ai_seat` synthesis (§7), and the '
+ 'note here said so: "Neither confers administrator standing." That sentence is now FALSE. '
+ 'The config anchor (§6b-config) matches the row\'s own `email` against the deployment\'s '
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/security/authz-cache-posture.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe('#11968 authz cache posture — the loud arm', () => {
expect(message).toContain('7500ms');
expect(message).toContain(AUTHZ_GRANTS_CACHE_TTL_ENV);
expect(message).toMatch(/not an error/i);
expect(message).toContain('#4785');
expect(message).toContain('a silently-absent invalidation bridge is how a security control gets disabled without anyone noticing.');
});
});

Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/security/authz-cache-posture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,8 @@ export function resolveAuthzCachePosture(
`another replica is honoured by this one for up to ${ttlMs}ms. That is a ` +
'supported configuration, not an error: the TTL is the correctness bound ' +
'and it still holds. It is stated because a silently-absent invalidation ' +
'bridge is how a security control gets disabled without anyone noticing ' +
`(#4785). To narrow the typical window, configure a remote cluster driver; ` +
'bridge is how a security control gets disabled without anyone noticing. ' +
`To narrow the typical window, configure a remote cluster driver; ` +
`to remove it entirely, set ${AUTHZ_GRANTS_CACHE_TTL_ENV}=0.`,
};
}
Expand Down
20 changes: 10 additions & 10 deletions packages/drivers/driver-memory/src/filter-refusal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export function refusePerAggregationFilter(alias: string): never {
`Per-aggregation \`filter\` on "${alias}" is not supported by this backend (driver-memory). ` +
`The query is spelled correctly and @objectstack/spec AggregationNodeSchema declares the key — ` +
`this backend compiles no conditional-aggregate (SQL FILTER (WHERE …) / CASE WHEN) expression ` +
`for it, so it is refused rather than silently aggregating the UNFILTERED rows (#10413), which ` +
`for it, so it is refused rather than silently aggregating the UNFILTERED rows, which ` +
`is why it answers NOT_IMPLEMENTED/501 rather than a 400. \`engine.aggregate\` lowers filtered ` +
`aggregations in memory for every driver without native support — route the query through the ` +
`engine, or drop the \`filter\` key.`,
Expand Down Expand Up @@ -219,7 +219,7 @@ export function filterArrayReachedDriverError(filters: unknown[]): Error {
`A filter ARRAY reached the driver: ${JSON.stringify(filters)}. ` +
`'where' is a FilterCondition object; the array form ('FilterArray') is input-only ` +
`authoring sugar and is lowered by @objectstack/spec parseFilterAST() at the engine ` +
`and protocol doors before any driver sees it (#5158). This driver no longer carries a ` +
`and protocol doors before any driver sees it. This driver no longer carries a ` +
`second compiler for it — call through ObjectQL, or lower the value yourself with ` +
`parseFilterAST(). Note the INFIX join form ([condA, "or", condB]) has no lowering at ` +
`all: write the prefix form ["or", condA, condB].`,
Expand Down Expand Up @@ -280,7 +280,7 @@ export function emptyFieldConstraintError(field: string, path: string): Error {
`comparand (e.g. { "${field}": "value" }). It is refused rather than evaluated because the ` +
`backends disagreed on what it means — driver-sql dropped it inside $and/$or/$not (matching ` +
`EVERY row) while refusing it at the top level, and this driver / @objectstack/formula ` +
`answered "matches nothing". #5240.`,
`answered "matches nothing".`,
);
}

Expand Down Expand Up @@ -447,8 +447,8 @@ export function uncompilableFieldOperatorError(
`but cannot be compiled by ${capabilities.face}. Supported operators on this surface: ` +
`${supported}. It is refused rather than dropped: a predicate that compiles to nothing does ` +
`not narrow the query, it WIDENS it — the aggregate is then computed over rows the filter ` +
`excluded, and a chart drawn over them looks like a working chart (#3948, #4286/ADR-0078, ` +
`#5345). Rewrite the predicate with a supported operator, or run it through find().`,
`excluded, and a chart drawn over them looks like a working chart (ADR-0078). ` +
`Rewrite the predicate with a supported operator, or run it through find().`,
);
}

Expand All @@ -474,7 +474,7 @@ export function uncompilableCombinatorError(
`It is refused rather than ignored: dropping a combinator discards a whole branch of the ` +
`filter and WIDENS the result set, and "$not" is what compileCelToFilter emits for a CEL ` +
`"!expr" RLS read scope — a dropped one is an over-permissive read, not an inaccurate ` +
`number (#3948, #5345).`,
`number.`,
);
}

Expand Down Expand Up @@ -522,7 +522,7 @@ export function unknownFieldOperatorError(op: string, field: string, path: strin
`Supported operators: ${SUPPORTED_FIELD_OPERATOR_LIST}. ` +
`Refused at ${path} rather than handed to the query engine, which answers an unknown ` +
`operator with an error carrying no code and no status — a 500-shaped body for what is a ` +
`400-class client mistake (#5324).`,
`400-class client mistake.`,
);
}

Expand All @@ -540,7 +540,7 @@ export function unknownLogicalOperatorError(key: string, path: string): Error {
`declared logical operators $and, $or and $not (@objectstack/spec LOGICAL_OPERATORS); every ` +
`other key is a field name. It is refused rather than passed through to the query engine, ` +
`which would answer with an uncoded error — or, worse, evaluate an operator the Filter ` +
`Protocol never declared (#5324).`,
`Protocol never declared.`,
);
}

Expand Down Expand Up @@ -568,7 +568,7 @@ export function malformedBetweenError(field: string, value: unknown, path: strin
`It is refused rather than skipped: a range that compiles to no predicate answers with a ` +
`row count the author never asked for, and this driver's faces did not even agree on ` +
`WHICH — the live query path returned NO rows, the reference matcher returned EVERY row, ` +
`and a dropped AST comparison would have matched every record (#5328).`,
`and a dropped AST comparison would have matched every record.`,
);
}

Expand Down Expand Up @@ -614,7 +614,7 @@ export function nonBooleanNullComparandError(field: string, value: unknown, path
`compiled IS NULL (anything but false), this driver's query path and driver-mongodb ` +
`compiled IS NOT NULL (anything but true), and this driver's matcher dropped the ` +
`constraint entirely. Note "false" the STRING is truthy, so it landed on the side opposite ` +
`the false it was written to mean (#5347).`,
`the false it was written to mean.`,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ describe('multi-tenancy boot guard (#6915)', () => {
expect((err as Error).message).toContain('OS_MULTI_ORG_ENABLED');
expect((err as Error).message).toContain('OS_TENANCY_POSTURE');
expect((err as Error).message).toContain('@objectstack/driver-sql');
expect((err as Error).message).toContain('6915');
expect((err as Error).message).toContain('Rather than run unisolated,');
}
});

Expand Down
11 changes: 1 addition & 10 deletions packages/drivers/driver-memory/src/memory-tenancy-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,6 @@ import { resolveTenancyPosture } from '@objectstack/types';
/** Stable, matchable error code for the boot refusal. */
export const MULTI_TENANT_UNSUPPORTED_CODE = 'MEMORY_MULTI_TENANT_UNSUPPORTED';

const ISSUE_URL = 'https://github.com/objectstack-ai/objectstack/issues/6915';
const CALL_SEAM_ISSUE_URL = 'https://github.com/objectstack-ai/objectstack/issues/16589';

/**
* Thrown when the in-memory driver is asked to run in a multi-tenant deployment,
* or to serve one call under a tenant scope it cannot honour.
Expand All @@ -141,10 +138,6 @@ export class MemoryMultiTenantUnsupportedError extends Error {
` the driver fails at startup.\n`
: ` answering this call would read, update or delete records belonging to OTHER\n` +
` organizations. Rather than answer it unisolated, the driver refuses it.\n`;
const tracking =
seam === 'boot'
? ISSUE_URL
: `${CALL_SEAM_ISSUE_URL} (per-call refusal), ${ISSUE_URL} (no isolation here)`;
super(
`[driver-memory] ${headline}: this driver has NO row-level tenant isolation.\n` +
`\n` +
Expand All @@ -159,9 +152,7 @@ export class MemoryMultiTenantUnsupportedError extends Error {
` deployments — it enforces tenant scoping at the driver level. For an\n` +
` in-process store, \`SqlDriver\` with \`connection: { filename: ':memory:' }\`\n` +
` is the closest drop-in replacement.\n` +
` ${remedy}\n` +
`\n` +
` Tracking: ${tracking}`,
` ${remedy}`,
);
this.name = 'MemoryMultiTenantUnsupportedError';
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,11 +190,11 @@ describe('per-call tenant-scope refusal (#16589)', () => {
expect(message).toContain(SCOPED_OBJECT);
expect(message).toContain(ORG_A);
// The remedy must name the isolating driver and the ADR-0066 opt-out,
// and must reach the tracking card — a refusal that does not say what to
// do next is the "loud" half without the "locatable" half.
// and the message must state the decision in words — a refusal that
// does not say what to do next is the "loud" half without the rest.
expect(message).toContain('@objectstack/driver-sql');
expect(message).toContain('tenancy: { enabled: false }');
expect(message).toContain('16589');
expect(message).toContain('Rather than answer it unisolated, the driver refuses it.');
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ describe('MongoDB Aggregation Pipeline Builder', () => {
// door has no other way to learn the name was RETIRED rather than
// merely misspelled.
expect(thrown!.message).toContain('was REMOVED');
expect(thrown!.message).toContain('#6188');
expect(thrown!.message).toContain('(ADR-0049 enforce-or-remove: no SQL backend compiled it)');
});
}

Expand Down
Loading
Loading