From 3432973ff19ade8d9226dfdfa93f1c8862c09815 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 10:59:29 +0000 Subject: [PATCH 1/3] fix(objectql): author-visible refusals and metadata text state each decision in words instead of a tracker number (stage 3, part 1) The engine's unknown-option and filter-array refusals, the two bulk-write row-scoping refusals, the credential-aggregation refusal, the HAVING operator refusal, the empty hook target, the hook-target rebind refusal, the strict read-only refusal, the system-write organization refusal, the lifecycle retention-override setting description and the search companion field description no longer cite tracker numbers. Where the sentence did not already say what was decided, it now does. The findOne refusal keeps its citation: metadata-core's byte-identical twin is compared in this package's tests, and that twin is a later stage's row. Text only. The prose-id ledger is recomputed with --census-ledger. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude --- .../engine-dropped-fields-primary-key.test.ts | 2 +- packages/objectql/src/engine.ts | 16 +++++---- packages/objectql/src/having-filter.ts | 2 +- packages/objectql/src/hook-binder.ts | 2 +- .../objectql/src/hook-target-rebind-errors.ts | 10 +++--- .../src/lifecycle/lifecycle-settings.ts | 2 +- .../objectql/src/readonly-strict-errors.ts | 4 +-- packages/objectql/src/search-companion.ts | 2 +- .../src/tenancy/system-write-organization.ts | 2 +- scripts/doc-authoring-prose-id.baseline.json | 33 +------------------ 10 files changed, 25 insertions(+), 50 deletions(-) diff --git a/packages/objectql/src/engine-dropped-fields-primary-key.test.ts b/packages/objectql/src/engine-dropped-fields-primary-key.test.ts index bdcd06249c9..4b20e146424 100644 --- a/packages/objectql/src/engine-dropped-fields-primary-key.test.ts +++ b/packages/objectql/src/engine-dropped-fields-primary-key.test.ts @@ -315,7 +315,7 @@ describe('#6437 — the refusal message is composed from `drops`, not from the c `API-boundary caller, isSystem included. A value DERIVED by a beforeUpdate hook is ` + `not a caller write and is never stripped — that is the sanctioned write path for a ` + `conditionally-locked derived field). To let the strip happen and merely observe it, drop ` + - `strictReadonlyWrites and pass options.onFieldsDropped instead (#3407).`, + `strictReadonlyWrites and pass options.onFieldsDropped instead.`, ); }); diff --git a/packages/objectql/src/engine.ts b/packages/objectql/src/engine.ts index 851cca296b6..7339de07436 100644 --- a/packages/objectql/src/engine.ts +++ b/packages/objectql/src/engine.ts @@ -770,7 +770,7 @@ function rejectUnknownEngineOptions( throw new Error( `${operation}('${object}') does not recognise option${unknown.length > 1 ? 's' : ''} ` + `${details.join('; ')}. The engine executes none of ${unknown.length > 1 ? 'them' : 'it'}, ` + - `so the call would succeed with the option silently ignored (#4371). ` + + `so the call would succeed with the option silently ignored. ` + `Legal keys for ${operation}: ${[...legal].sort().join(', ')}.`, ); } @@ -1071,7 +1071,7 @@ function lowerWhereFilterArray( `${JSON.stringify(where)}. A filter array is a comparison [field, operator, value], ` + `a logical node ["and"|"or", ...conditions], or a list of those — it is INPUT-ONLY ` + `sugar (spec 'FilterArray'), lowered to a FilterCondition here before any driver sees ` + - `it (#5158). This value cannot be lowered, and an unapplied filter would have returned ` + + `it. This value cannot be lowered, and an unapplied filter would have returned ` + `the UNFILTERED result set. Recognised operators: ` + `${[...VALID_AST_OPERATORS].sort().join(', ')}. Infix joins ([condA, "or", condB]) are ` + `NOT one of the shapes — write the prefix form ["or", condA, condB].`, @@ -1094,7 +1094,7 @@ function lowerWhereFilterArray( throw new Error( `${operation}('${object}'): filter array ${JSON.stringify(where)} passed isFilterAST() ` + `but parseFilterAST() lowered it to nothing. Refusing rather than running the query ` + - `unfiltered (#5158).`, + `unfiltered.`, ); } // [#5869] Door 2's half of the same check USED to be a second @@ -14017,7 +14017,9 @@ export class ObjectQL implements IObjectQLEngine { if (!ast) { throw new Error( `[Security] Refusing bulk update on '${object}': row-scoping AST was not seeded ` + - `(the predicate branch was reached without the #2982 seed).`, + `(the predicate branch was reached without the AST seeded before the middleware ` + + `chain — the one RLS and sharing compose their row-scoping onto, so that a bulk ` + + `write reaches only the rows this caller may edit).`, ); } // [#9974] The unscoped-multi shape check, BEFORE the matched-row @@ -16337,7 +16339,9 @@ export class ObjectQL implements IObjectQLEngine { if (!ast) { throw new Error( `[Security] Refusing bulk delete on '${object}': row-scoping AST was not seeded ` + - `(the predicate branch was reached without the #2982 seed).`, + `(the predicate branch was reached without the AST seeded before the middleware ` + + `chain — the one RLS and sharing compose their row-scoping onto, so that a bulk ` + + `write reaches only the rows this caller may edit).`, ); } // [#9719] The unscoped-multi shape check, BEFORE the matched-row read: @@ -16658,7 +16662,7 @@ export class ObjectQL implements IObjectQLEngine { + 'secret/password fields are masked on read and `internal: true` fields are omitted ' + 'outright, so the value never leaves the engine on the generic data path; aggregating ' + 'them (group-by, min/max, array_agg, …) would surface it. ' - + 'Refusing (fail-closed) — see ADR-0100 / #3171 / #7922.', + + 'Refusing (fail-closed) — see ADR-0100.', ); } } diff --git a/packages/objectql/src/having-filter.ts b/packages/objectql/src/having-filter.ts index 5dbf9111136..edd1d6b59b0 100644 --- a/packages/objectql/src/having-filter.ts +++ b/packages/objectql/src/having-filter.ts @@ -325,7 +325,7 @@ function unknownOperator( return invalidFilterError( `Unsupported operator '${op}' in \`${clause.root}\`. ${clause.semantics} and supports: ${supported}. ` + `An unknown operator is refused rather than ignored — ignoring it would silently ` - + `return unfiltered aggregates (#4286, ADR-0078).`, + + `return unfiltered aggregates (ADR-0078).`, ); } diff --git a/packages/objectql/src/hook-binder.ts b/packages/objectql/src/hook-binder.ts index 6f72007cea3..fbe6d57253f 100644 --- a/packages/objectql/src/hook-binder.ts +++ b/packages/objectql/src/hook-binder.ts @@ -205,7 +205,7 @@ export function bindHooksToEngine( result.skipped += 1; const reason = 'hook target names no object — an empty `object` is refused rather than widened to ' - + "the wildcard '*' (#4001). Name the object(s), or write `object: '*'` if firing on " + + "the wildcard '*'. Name the object(s), or write `object: '*'` if firing on " + 'every object is the intent.'; result.errors.push({ hook: hook.name, reason }); if (opts.strict) { diff --git a/packages/objectql/src/hook-target-rebind-errors.ts b/packages/objectql/src/hook-target-rebind-errors.ts index 280b6ccac23..890dcb26127 100644 --- a/packages/objectql/src/hook-target-rebind-errors.ts +++ b/packages/objectql/src/hook-target-rebind-errors.ts @@ -167,18 +167,20 @@ function buildMessage(info: { ? cleared ? ` The capability this used to have is RETIRED: clearing 'input.id' in a '${event}' handler ` + `converted a by-id write into a PREDICATE write over the caller's 'where'. Since ADR-0058 ` + - `Addendum II (#5574 / #5846) the dispatch ladder is resolved BEFORE the before phase — the ` + + `Addendum II the dispatch ladder is resolved BEFORE the before phase — the ` + `predicate path has to read its matched rows first, to build one context per row — so there ` + `is no ladder left to re-enter.` : ` The capability this used to have is RETIRED: rebinding 'input.id' in a '${event}' handler ` + `moved the write to another row. The engine now resolves the target BEFORE the before phase ` + `and computes the whole write against it — the pre-image, the 'readonlyWhen' locks, the ` + `validation rules — so a by-id target is immutable once a handler runs, on BOTH verbs. ` + - `'delete()' honoured a rebind until #6752 by re-resolving the new target; that is retired ` + - `too, so one rule now covers both.` + `'delete()' used to honour a rebind by re-resolving the new target; that is retired ` + + `too, because a handler that silently redirects which row gets deleted is a trap — so ` + + `one rule now covers both.` : path === 'unscoped-multi' ? ` This is the whole-operation dispatch an UNSCOPED predicate write delivers to a declared ` + - `shape guard (#9719, both write verbs since #9974): its 'id' is present-but-undefined ON ` + + `shape guard (one registered with 'dispatchUnscopedMultiWrite', on update and delete ` + + `alike): its 'id' is present-but-undefined ON ` + `PURPOSE — there is no target row — and the dispatch ladder was resolved before any handler ` + `ran, so binding 'input.id' here retargets nothing. It is refused rather than ignored, ` + `because a silent no-op is the failure this contract exists to abolish.` diff --git a/packages/objectql/src/lifecycle/lifecycle-settings.ts b/packages/objectql/src/lifecycle/lifecycle-settings.ts index d70335b7c70..7e0142ed772 100644 --- a/packages/objectql/src/lifecycle/lifecycle-settings.ts +++ b/packages/objectql/src/lifecycle/lifecycle-settings.ts @@ -46,7 +46,7 @@ export const lifecycleSettingsManifest = { 'Per-object window overrides: { "": { "maxAge": "1y", "expireAfter": "30d" } }. ' + 'Duration literals: h/d/w/y. Tenant-scoped — a regulated tenant sets years while dev keeps days (ADR-0057 §3.2). ' + 'An override BELOW a retention floor a consumer registered (e.g. the job queue\'s dedup window) is rejected at ' + - 'sweep time and logged at error — the declared window keeps running (#5195).', + 'sweep time and logged at error — the declared window keeps running.', }, { type: 'json', diff --git a/packages/objectql/src/readonly-strict-errors.ts b/packages/objectql/src/readonly-strict-errors.ts index b897b0a19cd..c02452d2cab 100644 --- a/packages/objectql/src/readonly-strict-errors.ts +++ b/packages/objectql/src/readonly-strict-errors.ts @@ -123,7 +123,7 @@ function buildRefusalMessage( const head = `${operation === 'insert' ? 'Insert' : 'Update'} on '${object}' was REFUSED: `; const tail = `To let the strip happen and merely observe it, drop ` + - `strictReadonlyWrites and pass options.onFieldsDropped instead (#3407).`; + `strictReadonlyWrites and pass options.onFieldsDropped instead.`; // Empty `drops` cannot happen on either throw site, but `every` on it is // vacuously true, which lands on the historical wording — the safe default. @@ -138,7 +138,7 @@ function buildRefusalMessage( (operation === 'insert' ? `{ context: { isSystem: true } } — or, for a data migration reinstating legacy ` + `values for a runtime-owned field (a record number), the historical-import ` + - `context { context: { preserveAudit: true } } (#3493). ` + `context { context: { preserveAudit: true } }. ` : `{ context: { isSystem: true } } (this exempts statically 'readonly' fields, but NOT ` + `fields locked by a TRUE 'readonlyWhen' predicate — those stay locked for every ` + `API-boundary caller, isSystem included. A value DERIVED by a beforeUpdate hook is ` + diff --git a/packages/objectql/src/search-companion.ts b/packages/objectql/src/search-companion.ts index 57b358a8d07..8d39ac91b3a 100644 --- a/packages/objectql/src/search-companion.ts +++ b/packages/objectql/src/search-companion.ts @@ -323,7 +323,7 @@ export function provisionSearchCompanion(schema: searchable: false, description: `Search-normalized forms of the display/name field (normalizers: ${SEARCH_COMPANION_NORMALIZERS.join(', ')}) — ` + - 'e.g. full pinyin + initials for CJK names. Maintained by plugin-pinyin-search; never hand-edited. See #2486.', + 'e.g. full pinyin + initials for CJK names. Maintained by plugin-pinyin-search; never hand-edited.', }, }, }; diff --git a/packages/objectql/src/tenancy/system-write-organization.ts b/packages/objectql/src/tenancy/system-write-organization.ts index 799beb2c296..f20365d5b7f 100644 --- a/packages/objectql/src/tenancy/system-write-organization.ts +++ b/packages/objectql/src/tenancy/system-write-organization.ts @@ -302,7 +302,7 @@ function buildRefusalMessage( `${DEFAULT_TENANT_FIELD} = NULL, which the autonumber counter and the partitioned unique index ` + `(COALESCE(${DEFAULT_TENANT_FIELD}, '${GLOBAL_TENANT}'), ) both collapse to the ` + `'${GLOBAL_TENANT}' pseudo-tenant — a second counter that cannot see the organization's own, so a ` + - `field declared unique silently gets the same value twice (#8844). Nothing was written. Fix it by ` + + `field declared unique silently gets the same value twice. Nothing was written. Fix it by ` + `carrying the organization the way a session write does: pass it on the execution context ` + `({ context: { isSystem: true, tenantId: '' } }), or set ${DEFAULT_TENANT_FIELD} ` + `on the record itself. If rows of '${object}' are genuinely platform-global and belong to no ` + diff --git a/scripts/doc-authoring-prose-id.baseline.json b/scripts/doc-authoring-prose-id.baseline.json index 12311965e9d..a7d4eaa217c 100644 --- a/scripts/doc-authoring-prose-id.baseline.json +++ b/scripts/doc-authoring-prose-id.baseline.json @@ -313,54 +313,23 @@ "#4246": 1 }, "packages/objectql/src/engine.ts": { - "#2982": 2, - "#3171": 1, "#3438": 2, "#3617": 2, - "#4371": 1, "#4419": 1, "#4769": 2, "#4797": 2, - "#5158": 2, "#5351": 1, - "#7413": 1, - "#7922": 1 - }, - "packages/objectql/src/having-filter.ts": { - "#4286": 1 - }, - "packages/objectql/src/hook-binder.ts": { - "#4001": 1 - }, - "packages/objectql/src/hook-target-rebind-errors.ts": { - "#5574": 1, - "#5846": 1, - "#6752": 1, - "#9719": 1, - "#9974": 1 + "#7413": 1 }, "packages/objectql/src/integrity/dangling-reference-audit.ts": { "#4551": 1 }, - "packages/objectql/src/lifecycle/lifecycle-settings.ts": { - "#5195": 1 - }, "packages/objectql/src/plugin.ts": { "#2462": 1 }, - "packages/objectql/src/readonly-strict-errors.ts": { - "#3407": 1, - "#3493": 1 - }, "packages/objectql/src/registry.ts": { "#3543": 1 }, - "packages/objectql/src/search-companion.ts": { - "#2486": 1 - }, - "packages/objectql/src/tenancy/system-write-organization.ts": { - "#8844": 1 - }, "packages/objectql/src/validation/rule-validator.ts": { "#2948": 1, "#3407": 2, From bccd37a25752f634722a2c250f9098807da9858a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 11:01:12 +0000 Subject: [PATCH 2/3] fix(objectql): log lines state each decision in words instead of a tracker number (stage 3, part 2) The ADR-0104 value-shape gate lines, the non-atomic cascade warning, the system-ledger transaction carve-out, the dangling-reference audit summary, the delegated protocol assembly line, the legacy apiMethods warning, the read-only and runtime-owned strip warnings and the two unevaluable-rule warnings no longer cite tracker numbers. Where the sentence did not already say what was decided, it now does. Two tests that pinned a number now pin the sentence. Text only. The prose-id ledger is recomputed with --census-ledger. Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude --- packages/objectql/src/engine.ts | 22 ++++++++-------- .../dangling-reference-audit.test.ts | 8 +++--- .../src/integrity/dangling-reference-audit.ts | 2 +- packages/objectql/src/plugin.ts | 2 +- packages/objectql/src/registry.test.ts | 2 +- packages/objectql/src/registry.ts | 5 ++-- .../objectql/src/validation/rule-validator.ts | 24 +++++++++--------- scripts/doc-authoring-prose-id.baseline.json | 25 +------------------ 8 files changed, 35 insertions(+), 55 deletions(-) diff --git a/packages/objectql/src/engine.ts b/packages/objectql/src/engine.ts index 7339de07436..036856ead49 100644 --- a/packages/objectql/src/engine.ts +++ b/packages/objectql/src/engine.ts @@ -9616,7 +9616,7 @@ export class ObjectQL implements IObjectQLEngine { FILE_REFERENCES_MIGRATION_ID, '[value-shape] this deployment has verified the file-as-reference migration — ' + 'media value shapes are enforced and released field files may be collected ' + - '(ADR-0104 / #3617)', + '(ADR-0104)', ); } @@ -9664,7 +9664,7 @@ export class ObjectQL implements IObjectQLEngine { 'valueShapesMigrationVerified', VALUE_SHAPES_MIGRATION_ID, '[value-shape] this deployment has verified the value-shape scan — reference and ' + - 'structured-JSON value shapes are enforced (ADR-0104 / #3438)', + 'structured-JSON value shapes are enforced (ADR-0104)', ); } @@ -9967,7 +9967,7 @@ export class ObjectQL implements IObjectQLEngine { 'no byte is deleted on evidence this deployment has contradicted. Fix the data and run ' + '`os migrate ' + (migrationId === FILE_REFERENCES_MIGRATION_ID ? 'files-to-references' : 'value-shapes') + - ' --apply` to clear it (ADR-0104 / #4797).', + ' --apply` to clear it (ADR-0104).', ); }) .catch((err: any) => { @@ -9978,7 +9978,7 @@ export class ObjectQL implements IObjectQLEngine { `[value-shape] could not record the observed deviation for '${migrationId}' ` + `(${err?.message ?? err}) — the ledger still authorises irreversible collection while ` + 'this deployment holds a value its own contract rejects; run the migration to ' + - 're-derive the gate (#4797)', + 're-derive the gate', ); }); } @@ -10069,7 +10069,7 @@ export class ObjectQL implements IObjectQLEngine { `(${tally?.first.object}.${tally?.first.field}: ${tally?.first.detail}). ` + 'The gate is closed again — fix the data, then run `os migrate ' + (migrationId === FILE_REFERENCES_MIGRATION_ID ? 'files-to-references' : 'value-shapes') + - ' --apply` to re-earn it (ADR-0104 / #4769).', + ' --apply` to re-earn it (ADR-0104).', ); }) .catch((err: any) => { @@ -10078,7 +10078,7 @@ export class ObjectQL implements IObjectQLEngine { this.logger.warn( `[value-shape] could not revoke the creation attestation for '${migrationId}' ` + `(${err?.message ?? err}) — the ledger still claims this deployment is verified ` + - 'while its data contradicts that; run the migration to re-derive it (#4769)', + 'while its data contradicts that; run the migration to re-derive it', ); }); } @@ -10154,7 +10154,7 @@ export class ObjectQL implements IObjectQLEngine { '[value-shape] media values are checked but NOT enforced here, and released files are ' + 'never collected — this deployment has not verified its file migration. Run ' + '`os migrate files-to-references` (dry run) to see what it would do, then `--apply` ' + - 'to close the gate (ADR-0104 / #3617).', + 'to close the gate (ADR-0104).', ); } if (covered && !(await this.readMigrationFlagVerified(VALUE_SHAPES_MIGRATION_ID)).verified) { @@ -10162,7 +10162,7 @@ export class ObjectQL implements IObjectQLEngine { '[value-shape] reference and structured-JSON values are checked but NOT enforced here — ' + 'this deployment has not verified its value-shape scan. Run `os migrate value-shapes` ' + '(dry run) to see what it would report, then `--apply` to close the gate ' + - '(ADR-0104 / #3438).', + '(ADR-0104).', ); } } catch { @@ -15201,7 +15201,8 @@ export class ObjectQL implements IObjectQLEngine { `Cascade delete of '${object}' cannot run as one unit of work: the cascade reaches an object routed ` + `to a datasource other than the default one ('${this.defaultDriver ?? ''}'), and a transaction ` + "covers one driver's connection only (ADR-0119 D1 — no two-phase commit). The cascade therefore runs " + - 'UNWRAPPED, exactly as it did before #7413: if a later dependent refuses the delete, the rows already ' + + 'UNWRAPPED, as every cascade did before a single-datasource cascade was made one transaction: if a ' + + 'later dependent refuses the delete, the rows already ' + 'removed stay removed while the call rejects. Route the cascading objects to one datasource to get the ' + 'atomic path. Reported once per object per engine instance.', { object, defaultDatasource: this.defaultDriver ?? undefined }, @@ -17426,7 +17427,8 @@ export class ObjectQL implements IObjectQLEngine { this.logger.debug( `${operation} of '${objectName}' inside transaction() is routed to datasource '${target}' while the ` + `transaction is open on '${scope.datasource}' — executing it OUTSIDE the transaction, on its own ` + - 'connection (ADR-0057 §3.6 system ledger, carved out by #5351). It commits independently and will ' + + 'connection (ADR-0057 §3.6 system ledger — the one class carved out of the cross-datasource write ' + + 'refusal). It commits independently and will ' + 'SURVIVE a rollback of this transaction: an audit/telemetry/event row may describe a write that was ' + 'undone. That is the decided direction of error for an append-only ledger — an extra reconcilable ' + 'row beats a missing row for a write that did commit. Said once per transaction per datasource.', diff --git a/packages/objectql/src/integrity/dangling-reference-audit.test.ts b/packages/objectql/src/integrity/dangling-reference-audit.test.ts index 261af66b224..92911b1033e 100644 --- a/packages/objectql/src/integrity/dangling-reference-audit.test.ts +++ b/packages/objectql/src/integrity/dangling-reference-audit.test.ts @@ -374,7 +374,7 @@ describe('[#4551] dangling stored references are reported, never rewritten', () }); await auditDanglingReferences(dirty); expect(dirty.warnings).toHaveLength(1); - expect(dirty.warnings[0][0]).toContain('#4551'); + expect(dirty.warnings[0][0]).toContain('reported, never rewritten'); expect((dirty.warnings[0][1] as any).references).toEqual([ 'sys_position_permission_set#ppr_1.permission_set_id → sys_permission_set#ps_gone', ]); @@ -495,7 +495,7 @@ describe('[#4747] a run that was called off is not a finding about the data', () expect(out.unreadableObjects).toEqual([]); // The real finding is still reported, and the summary line carries the // incompleteness so the log cannot read as a finished run either. - const summary = port.warnings.find((w) => w[0].includes('#4551')); + const summary = port.warnings.find((w) => w[0].includes('reported, never rewritten')); expect(summary).toBeDefined(); expect((summary![1] as any).aborted).toBe(true); }); @@ -712,7 +712,7 @@ describe('[#4743] provenance references are audited, in their OWN bucket', () => const out = await auditDanglingReferences(port); - const summary = port.warnings.find((w) => w[0].includes('#4551')); + const summary = port.warnings.find((w) => w[0].includes('reported, never rewritten')); expect(summary).toBeDefined(); const meta = summary![1] as Record; expect(meta.dangling).toBe(1); @@ -956,7 +956,7 @@ describe('[#5718] objects a finite budget never reached are named, not dropped', }); await auditDanglingReferences(loud, { maxRows: 1 }); - const summary = loud.warnings.find((w) => w[0].includes('#4551')); + const summary = loud.warnings.find((w) => w[0].includes('reported, never rewritten')); expect(summary).toBeDefined(); const meta = summary![1] as Record; // Itemised, not merely counted: object-scale, and a reader who has to act diff --git a/packages/objectql/src/integrity/dangling-reference-audit.ts b/packages/objectql/src/integrity/dangling-reference-audit.ts index e6595284a59..deab1c943dc 100644 --- a/packages/objectql/src/integrity/dangling-reference-audit.ts +++ b/packages/objectql/src/integrity/dangling-reference-audit.ts @@ -733,7 +733,7 @@ export async function auditDanglingReferences( // They ride along whenever the line fires for a real finding; the full // report always carries them for a caller that came looking. if (report.dangling.length || report.undetermined || report.unreadableObjects.length) { - port.warn?.('[integrity] stored references that resolve to nothing (#4551)', { + port.warn?.('[integrity] stored references that resolve to nothing — reported, never rewritten: a system-context write is exempt from the write-time reference check, so this audit is where such a reference surfaces', { scanned: report.scanned, dangling: report.dangling.length, undetermined: report.undetermined, diff --git a/packages/objectql/src/plugin.ts b/packages/objectql/src/plugin.ts index 98afc50a9ca..6530671021e 100644 --- a/packages/objectql/src/plugin.ts +++ b/packages/objectql/src/plugin.ts @@ -495,7 +495,7 @@ export class ObjectQLPlugin implements Plugin { }); this.subscribeMetadataRebind(ctx, protocolShim); } else { - ctx.logger.info('registerProtocol=false — protocol assembly delegated to MetadataProtocolPlugin (ADR-0076 Step 2, #2462)'); + ctx.logger.info('registerProtocol=false — protocol assembly delegated to MetadataProtocolPlugin (ADR-0076 Step 2)'); } // ADR-0057: the platform-owned LifecycleService. Registered from the diff --git a/packages/objectql/src/registry.test.ts b/packages/objectql/src/registry.test.ts index 989f0d8c399..cd024dcc672 100644 --- a/packages/objectql/src/registry.test.ts +++ b/packages/objectql/src/registry.test.ts @@ -1198,7 +1198,7 @@ describe('warnStrippedLegacyApiMethods (#3543)', () => { expect(warn.mock.calls[0][0]).toContain('import'); expect(warn.mock.calls[0][0]).toContain('export'); expect(warn.mock.calls[0][0]).toContain('IGNORED'); - expect(warn.mock.calls[0][0]).toContain('#3543'); + expect(warn.mock.calls[0][0]).toContain('derived from them or retired'); // primitives remain, so no deny-all escalation expect(warn.mock.calls[0][0]).not.toContain('deny-all'); }); diff --git a/packages/objectql/src/registry.ts b/packages/objectql/src/registry.ts index b0af60d72dc..9ea529d0526 100644 --- a/packages/objectql/src/registry.ts +++ b/packages/objectql/src/registry.ts @@ -1069,8 +1069,9 @@ export function warnStrippedLegacyApiMethods( const warn = opts?.warn ?? ((msg: string) => console.warn(msg)); warn( `[Registry] Object "${name}" declares retired legacy apiMethods value(s) ` + - `[${legacy.join(', ')}] in enable.apiMethods — since the enum shrink ` + - `(#3543) these are IGNORED: the effective API surface derives from the ` + + `[${legacy.join(', ')}] in enable.apiMethods — the authorable values are now the six ` + + `primitives only, because every other operation is derived from them or retired, so these ` + + `are IGNORED: the effective API surface derives from the ` + `six primitives (get/list/create/update/delete/bulk) alone ` + `(['create','update'] ⇒ upsert/import; ['list'] ⇒ aggregate/search/export; ` + `['get'] + trackHistory ⇒ history).` + diff --git a/packages/objectql/src/validation/rule-validator.ts b/packages/objectql/src/validation/rule-validator.ts index 90cec7aea79..7af815c4d4b 100644 --- a/packages/objectql/src/validation/rule-validator.ts +++ b/packages/objectql/src/validation/rule-validator.ts @@ -2564,8 +2564,8 @@ function preserveAuditRemedySentence(options?: StripWarningOptions): string { if (options?.preserveAuditApplies !== true) return ''; return ( ` A historical import restoring this record's own earlier values does NOT need that blanket ` + - `exemption: pass the narrower historical-import context { context: { preserveAudit: true } } ` + - `(#3493), which reinstates THIS field while the rest of the strip stays in force.` + `exemption: pass the narrower historical-import context { context: { preserveAudit: true } }, ` + + `which reinstates THIS field while the rest of the strip stays in force.` ); } @@ -2578,9 +2578,9 @@ function preserveAuditRemedySentence(options?: StripWarningOptions): string { function observeInsteadSentence(options?: StripWarningOptions): string { return options?.strict === true ? ` To let the strip happen and merely observe it instead of refusing the write, drop ` + - `options.strictReadonlyWrites and pass options.onFieldsDropped (#3407).` + `options.strictReadonlyWrites and pass options.onFieldsDropped.` : ` To detect drops programmatically instead of reading ` + - `this log, pass options.onFieldsDropped (#3407).`; + `this log, pass options.onFieldsDropped.`; } /** @@ -2616,10 +2616,10 @@ export function runtimeOwnedStripWarning( ? `DROPPED and the write is being REFUSED ENTIRELY — the runtime issues this value from its ` + `sequence, and this write passed options.strictReadonlyWrites, so NOTHING is written: not ` + `this column, and not the fields that would have survived the strip. The call throws ` + - `ERR_READONLY_FIELD_REJECTED rather than returning success (#5126).` + `ERR_READONLY_FIELD_REJECTED rather than returning success.` : `DROPPED and the write is being COMMITTED WITHOUT IT — the runtime issues this value from its ` + `sequence, so the call returns success while the column holds the generated number, not the one ` + - `sent (#5503).`; + `sent.`; return ( `Field '${field}'${on} is a runtime-owned '${type}' field: the caller-supplied value was ` + consequence + @@ -2627,8 +2627,8 @@ export function runtimeOwnedStripWarning( `declare itself trusted by passing { context: { isSystem: true } }` + (audit ? `; a data import reinstating ` + - `legacy record numbers uses the historical-import context ({ context: { preserveAudit: true } }, ` + - `#3493), which reinstates THIS field while the rest of the strip stays in force. ` + + `legacy record numbers uses the historical-import context ({ context: { preserveAudit: true } }), ` + + `which reinstates THIS field while the rest of the strip stays in force. ` + `A beforeInsert/beforeUpdate hook does NOT need either — hook-written keys are not ` + `caller-supplied.` : `. A beforeInsert/beforeUpdate hook does NOT need it — hook-written keys are not ` + @@ -2680,14 +2680,14 @@ export function readonlyStripWarning( ? `the caller-supplied value was DROPPED and the ${noun} is being REFUSED ENTIRELY — this ` + `write passed options.strictReadonlyWrites, so NOTHING is written: not this column, and ` + `not the fields that would have survived the strip. The call throws ` + - `ERR_READONLY_FIELD_REJECTED rather than returning success (#5126).` + `ERR_READONLY_FIELD_REJECTED rather than returning success.` : insert ? `the caller-supplied value was DROPPED and the create ` + `is being COMMITTED WITHOUT IT — the call returns success while this column takes its ` + `declared defaultValue instead of the value you sent.` : `the caller-supplied value was DROPPED and the update ` + `is being COMMITTED WITHOUT IT — the call returns success while this column keeps its stored ` + - `value (#2948).`; + `value.`; return ( `Field '${field}'${on} is read-only: ` + consequence + @@ -4065,7 +4065,7 @@ function checkPredicate( // Still logged — the operator needs the fault in the log even though the // caller now gets it in the response. Note the verb: rejected, not skipped. logger?.warn?.( - `Validation rule '${rule.name}' predicate failed to evaluate (${result.error.kind}: ${result.error.message}) — write rejected (#4649)`, + `Validation rule '${rule.name}' predicate failed to evaluate (${result.error.kind}: ${result.error.message}) — write rejected: a rule that cannot be evaluated fails closed, it is never skipped`, ); const unevaluable = unevaluableRuleError(rule.name, field, result.error, 'predicate'); // [#20006] Same verdict; on a delete's reference cleanup, a text that names it. @@ -4229,7 +4229,7 @@ function checkConditional( if (!result.ok) { ctx.logger?.warn?.( - `Validation rule '${rule.name}' when-predicate failed to evaluate (${result.error.kind}: ${result.error.message}) — write rejected (#4649)`, + `Validation rule '${rule.name}' when-predicate failed to evaluate (${result.error.kind}: ${result.error.message}) — write rejected: a rule that cannot be evaluated fails closed, it is never skipped`, ); return unevaluableRuleError(rule.name, '_record', result.error, 'when-predicate'); } diff --git a/scripts/doc-authoring-prose-id.baseline.json b/scripts/doc-authoring-prose-id.baseline.json index a7d4eaa217c..da4c5c61053 100644 --- a/scripts/doc-authoring-prose-id.baseline.json +++ b/scripts/doc-authoring-prose-id.baseline.json @@ -313,30 +313,7 @@ "#4246": 1 }, "packages/objectql/src/engine.ts": { - "#3438": 2, - "#3617": 2, - "#4419": 1, - "#4769": 2, - "#4797": 2, - "#5351": 1, - "#7413": 1 - }, - "packages/objectql/src/integrity/dangling-reference-audit.ts": { - "#4551": 1 - }, - "packages/objectql/src/plugin.ts": { - "#2462": 1 - }, - "packages/objectql/src/registry.ts": { - "#3543": 1 - }, - "packages/objectql/src/validation/rule-validator.ts": { - "#2948": 1, - "#3407": 2, - "#3493": 2, - "#4649": 2, - "#5126": 2, - "#5503": 1 + "#4419": 1 }, "packages/platform-objects/src/apps/translations/en.objects.generated.ts": { "#7987": 3, From 26848968cc5bd863ddcc6d9acf106afefee523d3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 30 Sep 2026 11:01:33 +0000 Subject: [PATCH 3/3] chore(changeset): objectql runtime strings state the decision (patch) Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude --- ...ctql-runtime-strings-state-the-decision.md | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .changeset/20513-objectql-runtime-strings-state-the-decision.md diff --git a/.changeset/20513-objectql-runtime-strings-state-the-decision.md b/.changeset/20513-objectql-runtime-strings-state-the-decision.md new file mode 100644 index 00000000000..dc22d334c56 --- /dev/null +++ b/.changeset/20513-objectql-runtime-strings-state-the-decision.md @@ -0,0 +1,35 @@ +--- +'@objectstack/objectql': patch +--- + +objectql refusals, log lines and metadata text no longer cite tracker numbers; each states the reason in words + +Clause-②: no + +Many messages the query engine shows 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: + +- Refusals: the bulk update and bulk delete row-scoping refusals now name the seed they are missing + (the AST seeded before the middleware chain, which RLS and sharing compose their row-scoping onto, + so a bulk write reaches only the rows the caller may edit); the hook-target rebind refusal says + why `delete()` stopped honouring a rebind (a handler that silently redirects which row gets + deleted is a trap) and names the `dispatchUnscopedMultiWrite` registration the whole-operation + dispatch goes to, on update and delete alike. The unknown-option, filter-array, + credential-aggregation, HAVING-operator, empty-hook-target, strict read-only and system-write + organization refusals lose only the citation, because their sentences already said it. +- Metadata text: the lifecycle `retention_overrides` setting description and the search companion + field description lose their citation. +- Log lines: the non-atomic cascade warning says a single-datasource cascade is now one + transaction; the system-ledger transaction line calls the ledger the one class carved out of the + cross-datasource write refusal; the dangling-reference audit summary says findings are reported, + never rewritten, because a system-context write is exempt from the write-time reference check; + the legacy `apiMethods` warning says the authorable values are the six primitives only, every + other operation being derived from them or retired; the two unevaluable-rule warnings say such a + rule fails closed and is never skipped. The ADR-0104 value-shape gate lines, the delegated + protocol-assembly line and the read-only and runtime-owned strip warnings lose only the citation. + +The `findOne` no-predicate refusal keeps its citation for now: `@objectstack/metadata-core` +carries a byte-identical copy that this package's tests compare against, and both move together. + +Text only: no error code, field name, status or behaviour changes.