Skip to content

Commit a94f3ba

Browse files
fix(objectql): refusals, log lines and metadata text state each decision in words instead of a tracker number (stage 3) (#20848)
Part of #20513 Clause-②: no **Stage 3 of 5 of this lane (`objectql`), under the maintainer's A / A ruling on the card.** The card stays open for stages 4-5, so this PR carries no closing keyword. Text only: no error `code`, field name, HTTP status, export or control flow moves. Every changed source line is a string-literal line, except two single-line log calls (`port.warn?.(...)` in the dangling-reference audit and `ctx.logger.info(...)` in `plugin.ts`) whose only changed token is the literal; stripping every string literal from the removed and the added lines leaves the same code skeleton apart from `+` joins for re-wrapped literals. ## What this does The query engine's refusals, metadata text and log lines sent the reader to a tracker number for the reason behind them. Each rewritten string now says that reason in words (form D, as the migration-entry rewrite and stages 1 and 2 applied it). Where the sentence already stated what was decided, only the citation goes. Where it did not, the decision is added in words: | Where | Cited | The sentence now says | |---|---|---| | `engine.ts` bulk update and bulk delete row-scoping refusals (thrown, `[Security]`) | 2982 | The missing seed is the AST seeded before the middleware chain, the one RLS and sharing compose their row-scoping onto, so a bulk write reaches only the rows this caller may edit. | | `hook-target-rebind-errors.ts` by-id REBOUND branch (thrown) | 6752 | `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. | | `hook-target-rebind-errors.ts` unscoped-multi branch (thrown) | 9719, 9974 | The whole-operation dispatch goes to a shape guard registered with `dispatchUnscopedMultiWrite`, on update and delete alike. | | `engine.ts` non-atomic cascade warning (`warn`) | 7413 | The cascade runs unwrapped, as every cascade did before a single-datasource cascade was made one transaction. | | `engine.ts` system-ledger write inside a transaction (`debug`) | 5351 | The ADR-0057 section 3.6 system ledger is the one class carved out of the cross-datasource write refusal. | | `integrity/dangling-reference-audit.ts` summary (`warn`) | 4551 | Findings are reported, never rewritten: a system-context write is exempt from the write-time reference check, so this audit is where such a reference surfaces. | | `registry.ts` legacy `apiMethods` warning (`warn`) | 3543 | The authorable values are now the six primitives only, because every other operation is derived from them or retired. | | `validation/rule-validator.ts` predicate and when-predicate evaluation failures (`warn`) | 4649 | Write rejected: a rule that cannot be evaluated fails closed, it is never skipped. | | everything else: the unknown-option, filter-array (two), credential-aggregation, HAVING-operator, empty-hook-target, hook-target CLEARED, strict read-only (two clauses) and system-write organization refusals; the lifecycle `retention_overrides` setting description and the search companion field description (metadata text); the eight ADR-0104 value-shape gate lines; the delegated protocol-assembly line; the read-only and runtime-owned strip warnings (eight clauses) | 4371, 5158, 3171, 7922, 4286, 4001, 5574, 5846, 3407, 3493, 8844, 5195, 2486, 3617, 3438, 4797, 4769, 2462, 5126, 5503, 2948 | The sentence already said what was decided; only the citation goes. Where an ADR stood beside the number (ADR-0100, ADR-0078, ADR-0104, ADR-0076 Step 2), the ADR stays. | Each claim was checked against today's code, not only against the cited card: the bulk-write AST is seeded before `executeWithMiddleware` on both verbs; `planCascadeAtomicity` returns `'atomic'` for a single-datasource cascade and `delete()` then runs it inside `transaction()`; a cross-datasource business write throws `CrossDatasourceTransactionWriteError` while a system-ledger object runs outside the transaction; the write-path reference check is non-system writes only; `LEGACY_API_METHODS` holds the eight derived or retired values; `dispatchUnscopedMultiWrite` is a registration option valid on `beforeUpdate` / `beforeDelete`; an unevaluable rule returns `unevaluableRuleError`. All 31 cited cards read closed as completed. ## One string held for a later stage `engine.ts`'s `findOne` no-predicate refusal keeps its citation (4419, 1 occurrence). `@objectstack/metadata-core`'s `engineFindOnePredicateRefusalMessage` is documented as byte-identical to it, and this package's `engine-findone-predicate.test.ts` compares the two (strict equality and `toThrow`). Moving the metadata-core copy is another package's ledger row, which this stage may not touch. A one-off ablation proves the hold: dropping the citation in `engine.ts` alone turns 9 of 24 tests in that file red. Stage 5 (the other seven packages, `metadata-core` among them) rewrites the pair together, and its ledger diff will move this `objectql` row too. ## Order inside the stage All 39 rewritten literals (42 occurrences) fit one PR, under the stop line, so the stage lands whole in three ordered commits, each recomputing the ledger so every commit is green on `check:doc-authoring`: 1. `3432973ff` author-visible text: 16 literals (19 occurrences), the thrown refusals and the two metadata texts, plus the one byte-exact re-pin; 2. `bccd37a25` log lines: 23 literals (23 occurrences), plus the two re-pinned tests; 3. `26848968c` the changeset. `objectql` has no ledgered `src/`-shipped test string: the census's 3 test-facing strings sit in `engine-data-events.bench.ts`, which the ledger excludes. ## Pins re-pinned: 6 assertion lines in 3 test files - `integrity/dangling-reference-audit.test.ts` 377, 498, 715, 959 found the summary warning by the number. They now find it by "reported, never rewritten". - `registry.test.ts` 1201 asserted the number in the legacy `apiMethods` warning. It now asserts "derived from them or retired". - `engine-dropped-fields-primary-key.test.ts` 318 is the byte-exact pin on the strict read-only refusal; it moves with the text and stays byte-exact. A one-off mutation proves each moved pin can fail, run on the committed head `26848968cc` with `scripts/ablation-replace.mjs` in wrap mode (anchor hit once, disk-verified) under a shell trap that restores each file from `HEAD`. The tests import `src` directly, so no build leg applies. "reported, never rewritten" to "reported, never repaired": 4 failed / 35 passed. "derived from them or retired" to "derived from them or dropped": 1 failed / 100 passed. "onFieldsDropped instead." to "onFieldsDropped instead!": 1 failed / 15 passed. After each leg the blob equals `HEAD` and `git diff HEAD` is empty; after the run `git status --porcelain` has 0 lines. No consumer outside the package pins a changed string. Every fragment of every removed source line was searched in all test files of the repository: the hits are comments, fragments the new text keeps, the package's own tests above, and `plugin-security`'s `auto-org-admin-grant.test.ts`, whose test double keeps its own copy of the unknown-option sentence and is never compared with the engine's (see Acceptance notes). ## Ledger burn-down `scripts/doc-authoring-prose-id.baseline.json` was regenerated with `--census-ledger`. Only `objectql` rows move; no other package's row changes. | File | Before | After | |---|---|---| | `src/engine.ts` | 18 | 1 (4419, held) | | `src/validation/rule-validator.ts` | 10 | 0 | | `src/hook-target-rebind-errors.ts` | 5 | 0 | | `src/readonly-strict-errors.ts` | 2 | 0 | | `src/tenancy/system-write-organization.ts` | 1 | 0 | | `src/search-companion.ts` | 1 | 0 | | `src/registry.ts` | 1 | 0 | | `src/plugin.ts` | 1 | 0 | | `src/lifecycle/lifecycle-settings.ts` | 1 | 0 | | `src/integrity/dangling-reference-audit.ts` | 1 | 0 | | `src/hook-binder.ts` | 1 | 0 | | `src/having-filter.ts` | 1 | 0 | | **objectql** | **43 in 12 files** | **1 in 1 file** | | whole ledger | 811 occurrences, 546 pairs, 215 files | 769 occurrences, 514 pairs, 204 files | The census's 38 messages for `objectql` are the 40 ledgered literals minus the two metadata texts it bucketed separately; the 40 literals carry 43 occurrences because three of them cite two numbers each. ## Verification (head `26848968cc`) - Build: `turbo run build` over `@objectstack/objectql` and its closure, 14/14; then the whole workspace (`./packages/*`, `./packages/*/*`), 71/71; then `@objectstack/objectql` directly after the ablation run touched its sources. The new sentences are in `dist/index.js`, and none of the removed citations remains in a string there (the matches left are comments). - `@objectstack/objectql` tests (`vitest run --project local --maxWorkers=2`, three shards): 115 + 115 + 114 files passed, 2253 + 2001 + 2515 tests passed, 0 failed. - `@objectstack/objectql` typecheck: exit 0 (`tsc --noEmit`, the scripts config, and `check:test-typecheck` holding its ledger). - `node scripts/pm/dispatch-gates.mjs --commands`: 76 derived commands, all run, all exit 0. `check:dual-build-cjs-loads` and `check:type-check-debt` first answered exit 3 (prerequisite not met) and were re-run after the full build and the direct `objectql` rebuild; `check:dts-closure` and `check:lean-entry-closure` were re-run there too. `--ran`: 76 derived, 76 run, 0 NOT-MEASURED, 0 UNRUN. - `check:doc-authoring`: sibling-package prose ids hold the baseline, no growth, no burn-down unrecorded (647 pinned sites across 204 files). - Narrowed lint: `eslint --no-inline-config --format json` over the 15 touched `.ts` files: 15 files, 0 errors, 0 warnings. The resolved `parserOptions` for `engine.ts` are `ecmaVersion: latest` and `sourceType: module` only, with no `project` and no `projectService`, so no type-aware rule can move an untouched file. Repo-wide `pnpm lint` is CI's. - Not measured locally (CI's): the `repo` vitest project's one file (`action-owner-key-single-source.test.ts`, untouched by this diff), the Test Core shards, Dogfood, and the workspace type-check lanes. ## Acceptance notes - **Held for stage 5:** the `findOne` refusal pair (`objectql` `engine.ts` and `metadata-core` `engine-findone-predicate.ts`), as above. Stage 5's file surface needs `packages/objectql/src/engine.ts` as well. - **Noted, not filed:** `plugin-security`'s `auto-org-admin-grant.test.ts` carries a test double of the engine's unknown-option refusal with its own copy of the sentence, citation included. It is a test file, outside the ledger and outside the ruling's shipped-`src/` scope, and no test compares it with the engine's text, so the two now differ by the citation only. Carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 40d6c5f commit a94f3ba

17 files changed

Lines changed: 94 additions & 104 deletions
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
'@objectstack/objectql': patch
3+
---
4+
5+
objectql refusals, log lines and metadata text no longer cite tracker numbers; each states the reason in words
6+
7+
Clause-②: no
8+
9+
Many messages the query engine shows to authors, administrators and operators ended with an
10+
issue-tracker number where the reason belonged. The number goes, and where the sentence did not
11+
already say what was decided, it now does:
12+
13+
- Refusals: the bulk update and bulk delete row-scoping refusals now name the seed they are missing
14+
(the AST seeded before the middleware chain, which RLS and sharing compose their row-scoping onto,
15+
so a bulk write reaches only the rows the caller may edit); the hook-target rebind refusal says
16+
why `delete()` stopped honouring a rebind (a handler that silently redirects which row gets
17+
deleted is a trap) and names the `dispatchUnscopedMultiWrite` registration the whole-operation
18+
dispatch goes to, on update and delete alike. The unknown-option, filter-array,
19+
credential-aggregation, HAVING-operator, empty-hook-target, strict read-only and system-write
20+
organization refusals lose only the citation, because their sentences already said it.
21+
- Metadata text: the lifecycle `retention_overrides` setting description and the search companion
22+
field description lose their citation.
23+
- Log lines: the non-atomic cascade warning says a single-datasource cascade is now one
24+
transaction; the system-ledger transaction line calls the ledger the one class carved out of the
25+
cross-datasource write refusal; the dangling-reference audit summary says findings are reported,
26+
never rewritten, because a system-context write is exempt from the write-time reference check;
27+
the legacy `apiMethods` warning says the authorable values are the six primitives only, every
28+
other operation being derived from them or retired; the two unevaluable-rule warnings say such a
29+
rule fails closed and is never skipped. The ADR-0104 value-shape gate lines, the delegated
30+
protocol-assembly line and the read-only and runtime-owned strip warnings lose only the citation.
31+
32+
The `findOne` no-predicate refusal keeps its citation for now: `@objectstack/metadata-core`
33+
carries a byte-identical copy that this package's tests compare against, and both move together.
34+
35+
Text only: no error code, field name, status or behaviour changes.

‎packages/objectql/src/engine-dropped-fields-primary-key.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ describe('#6437 — the refusal message is composed from `drops`, not from the c
315315
`API-boundary caller, isSystem included. A value DERIVED by a beforeUpdate hook is ` +
316316
`not a caller write and is never stripped — that is the sanctioned write path for a ` +
317317
`conditionally-locked derived field). To let the strip happen and merely observe it, drop ` +
318-
`strictReadonlyWrites and pass options.onFieldsDropped instead (#3407).`,
318+
`strictReadonlyWrites and pass options.onFieldsDropped instead.`,
319319
);
320320
});
321321

‎packages/objectql/src/engine.ts‎

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -770,7 +770,7 @@ function rejectUnknownEngineOptions(
770770
throw new Error(
771771
`${operation}('${object}') does not recognise option${unknown.length > 1 ? 's' : ''} ` +
772772
`${details.join('; ')}. The engine executes none of ${unknown.length > 1 ? 'them' : 'it'}, ` +
773-
`so the call would succeed with the option silently ignored (#4371). ` +
773+
`so the call would succeed with the option silently ignored. ` +
774774
`Legal keys for ${operation}: ${[...legal].sort().join(', ')}.`,
775775
);
776776
}
@@ -1071,7 +1071,7 @@ function lowerWhereFilterArray<T extends object | undefined>(
10711071
`${JSON.stringify(where)}. A filter array is a comparison [field, operator, value], ` +
10721072
`a logical node ["and"|"or", ...conditions], or a list of those — it is INPUT-ONLY ` +
10731073
`sugar (spec 'FilterArray'), lowered to a FilterCondition here before any driver sees ` +
1074-
`it (#5158). This value cannot be lowered, and an unapplied filter would have returned ` +
1074+
`it. This value cannot be lowered, and an unapplied filter would have returned ` +
10751075
`the UNFILTERED result set. Recognised operators: ` +
10761076
`${[...VALID_AST_OPERATORS].sort().join(', ')}. Infix joins ([condA, "or", condB]) are ` +
10771077
`NOT one of the shapes — write the prefix form ["or", condA, condB].`,
@@ -1094,7 +1094,7 @@ function lowerWhereFilterArray<T extends object | undefined>(
10941094
throw new Error(
10951095
`${operation}('${object}'): filter array ${JSON.stringify(where)} passed isFilterAST() ` +
10961096
`but parseFilterAST() lowered it to nothing. Refusing rather than running the query ` +
1097-
`unfiltered (#5158).`,
1097+
`unfiltered.`,
10981098
);
10991099
}
11001100
// [#5869] Door 2's half of the same check USED to be a second
@@ -9616,7 +9616,7 @@ export class ObjectQL implements IObjectQLEngine {
96169616
FILE_REFERENCES_MIGRATION_ID,
96179617
'[value-shape] this deployment has verified the file-as-reference migration — ' +
96189618
'media value shapes are enforced and released field files may be collected ' +
9619-
'(ADR-0104 / #3617)',
9619+
'(ADR-0104)',
96209620
);
96219621
}
96229622

@@ -9664,7 +9664,7 @@ export class ObjectQL implements IObjectQLEngine {
96649664
'valueShapesMigrationVerified',
96659665
VALUE_SHAPES_MIGRATION_ID,
96669666
'[value-shape] this deployment has verified the value-shape scan — reference and ' +
9667-
'structured-JSON value shapes are enforced (ADR-0104 / #3438)',
9667+
'structured-JSON value shapes are enforced (ADR-0104)',
96689668
);
96699669
}
96709670

@@ -9967,7 +9967,7 @@ export class ObjectQL implements IObjectQLEngine {
99679967
'no byte is deleted on evidence this deployment has contradicted. Fix the data and run ' +
99689968
'`os migrate ' +
99699969
(migrationId === FILE_REFERENCES_MIGRATION_ID ? 'files-to-references' : 'value-shapes') +
9970-
' --apply` to clear it (ADR-0104 / #4797).',
9970+
' --apply` to clear it (ADR-0104).',
99719971
);
99729972
})
99739973
.catch((err: any) => {
@@ -9978,7 +9978,7 @@ export class ObjectQL implements IObjectQLEngine {
99789978
`[value-shape] could not record the observed deviation for '${migrationId}' ` +
99799979
`(${err?.message ?? err}) — the ledger still authorises irreversible collection while ` +
99809980
'this deployment holds a value its own contract rejects; run the migration to ' +
9981-
're-derive the gate (#4797)',
9981+
're-derive the gate',
99829982
);
99839983
});
99849984
}
@@ -10069,7 +10069,7 @@ export class ObjectQL implements IObjectQLEngine {
1006910069
`(${tally?.first.object}.${tally?.first.field}: ${tally?.first.detail}). ` +
1007010070
'The gate is closed again — fix the data, then run `os migrate ' +
1007110071
(migrationId === FILE_REFERENCES_MIGRATION_ID ? 'files-to-references' : 'value-shapes') +
10072-
' --apply` to re-earn it (ADR-0104 / #4769).',
10072+
' --apply` to re-earn it (ADR-0104).',
1007310073
);
1007410074
})
1007510075
.catch((err: any) => {
@@ -10078,7 +10078,7 @@ export class ObjectQL implements IObjectQLEngine {
1007810078
this.logger.warn(
1007910079
`[value-shape] could not revoke the creation attestation for '${migrationId}' ` +
1008010080
`(${err?.message ?? err}) — the ledger still claims this deployment is verified ` +
10081-
'while its data contradicts that; run the migration to re-derive it (#4769)',
10081+
'while its data contradicts that; run the migration to re-derive it',
1008210082
);
1008310083
});
1008410084
}
@@ -10154,15 +10154,15 @@ export class ObjectQL implements IObjectQLEngine {
1015410154
'[value-shape] media values are checked but NOT enforced here, and released files are ' +
1015510155
'never collected — this deployment has not verified its file migration. Run ' +
1015610156
'`os migrate files-to-references` (dry run) to see what it would do, then `--apply` ' +
10157-
'to close the gate (ADR-0104 / #3617).',
10157+
'to close the gate (ADR-0104).',
1015810158
);
1015910159
}
1016010160
if (covered && !(await this.readMigrationFlagVerified(VALUE_SHAPES_MIGRATION_ID)).verified) {
1016110161
this.logger.info(
1016210162
'[value-shape] reference and structured-JSON values are checked but NOT enforced here — ' +
1016310163
'this deployment has not verified its value-shape scan. Run `os migrate value-shapes` ' +
1016410164
'(dry run) to see what it would report, then `--apply` to close the gate ' +
10165-
'(ADR-0104 / #3438).',
10165+
'(ADR-0104).',
1016610166
);
1016710167
}
1016810168
} catch {
@@ -14017,7 +14017,9 @@ export class ObjectQL implements IObjectQLEngine {
1401714017
if (!ast) {
1401814018
throw new Error(
1401914019
`[Security] Refusing bulk update on '${object}': row-scoping AST was not seeded ` +
14020-
`(the predicate branch was reached without the #2982 seed).`,
14020+
`(the predicate branch was reached without the AST seeded before the middleware ` +
14021+
`chain — the one RLS and sharing compose their row-scoping onto, so that a bulk ` +
14022+
`write reaches only the rows this caller may edit).`,
1402114023
);
1402214024
}
1402314025
// [#9974] The unscoped-multi shape check, BEFORE the matched-row
@@ -15199,7 +15201,8 @@ export class ObjectQL implements IObjectQLEngine {
1519915201
`Cascade delete of '${object}' cannot run as one unit of work: the cascade reaches an object routed ` +
1520015202
`to a datasource other than the default one ('${this.defaultDriver ?? '<none>'}'), and a transaction ` +
1520115203
"covers one driver's connection only (ADR-0119 D1 — no two-phase commit). The cascade therefore runs " +
15202-
'UNWRAPPED, exactly as it did before #7413: if a later dependent refuses the delete, the rows already ' +
15204+
'UNWRAPPED, as every cascade did before a single-datasource cascade was made one transaction: if a ' +
15205+
'later dependent refuses the delete, the rows already ' +
1520315206
'removed stay removed while the call rejects. Route the cascading objects to one datasource to get the ' +
1520415207
'atomic path. Reported once per object per engine instance.',
1520515208
{ object, defaultDatasource: this.defaultDriver ?? undefined },
@@ -16337,7 +16340,9 @@ export class ObjectQL implements IObjectQLEngine {
1633716340
if (!ast) {
1633816341
throw new Error(
1633916342
`[Security] Refusing bulk delete on '${object}': row-scoping AST was not seeded ` +
16340-
`(the predicate branch was reached without the #2982 seed).`,
16343+
`(the predicate branch was reached without the AST seeded before the middleware ` +
16344+
`chain — the one RLS and sharing compose their row-scoping onto, so that a bulk ` +
16345+
`write reaches only the rows this caller may edit).`,
1634116346
);
1634216347
}
1634316348
// [#9719] The unscoped-multi shape check, BEFORE the matched-row read:
@@ -16658,7 +16663,7 @@ export class ObjectQL implements IObjectQLEngine {
1665816663
+ 'secret/password fields are masked on read and `internal: true` fields are omitted '
1665916664
+ 'outright, so the value never leaves the engine on the generic data path; aggregating '
1666016665
+ 'them (group-by, min/max, array_agg, …) would surface it. '
16661-
+ 'Refusing (fail-closed) — see ADR-0100 / #3171 / #7922.',
16666+
+ 'Refusing (fail-closed) — see ADR-0100.',
1666216667
);
1666316668
}
1666416669
}
@@ -17422,7 +17427,8 @@ export class ObjectQL implements IObjectQLEngine {
1742217427
this.logger.debug(
1742317428
`${operation} of '${objectName}' inside transaction() is routed to datasource '${target}' while the ` +
1742417429
`transaction is open on '${scope.datasource}' — executing it OUTSIDE the transaction, on its own ` +
17425-
'connection (ADR-0057 §3.6 system ledger, carved out by #5351). It commits independently and will ' +
17430+
'connection (ADR-0057 §3.6 system ledger — the one class carved out of the cross-datasource write ' +
17431+
'refusal). It commits independently and will ' +
1742617432
'SURVIVE a rollback of this transaction: an audit/telemetry/event row may describe a write that was ' +
1742717433
'undone. That is the decided direction of error for an append-only ledger — an extra reconcilable ' +
1742817434
'row beats a missing row for a write that did commit. Said once per transaction per datasource.',

‎packages/objectql/src/having-filter.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ function unknownOperator(
325325
return invalidFilterError(
326326
`Unsupported operator '${op}' in \`${clause.root}\`. ${clause.semantics} and supports: ${supported}. `
327327
+ `An unknown operator is refused rather than ignored — ignoring it would silently `
328-
+ `return unfiltered aggregates (#4286, ADR-0078).`,
328+
+ `return unfiltered aggregates (ADR-0078).`,
329329
);
330330
}
331331

‎packages/objectql/src/hook-binder.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ export function bindHooksToEngine(
205205
result.skipped += 1;
206206
const reason =
207207
'hook target names no object — an empty `object` is refused rather than widened to '
208-
+ "the wildcard '*' (#4001). Name the object(s), or write `object: '*'` if firing on "
208+
+ "the wildcard '*'. Name the object(s), or write `object: '*'` if firing on "
209209
+ 'every object is the intent.';
210210
result.errors.push({ hook: hook.name, reason });
211211
if (opts.strict) {

‎packages/objectql/src/hook-target-rebind-errors.ts‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,18 +167,20 @@ function buildMessage(info: {
167167
? cleared
168168
? ` The capability this used to have is RETIRED: clearing 'input.id' in a '${event}' handler ` +
169169
`converted a by-id write into a PREDICATE write over the caller's 'where'. Since ADR-0058 ` +
170-
`Addendum II (#5574 / #5846) the dispatch ladder is resolved BEFORE the before phase — the ` +
170+
`Addendum II the dispatch ladder is resolved BEFORE the before phase — the ` +
171171
`predicate path has to read its matched rows first, to build one context per row — so there ` +
172172
`is no ladder left to re-enter.`
173173
: ` The capability this used to have is RETIRED: rebinding 'input.id' in a '${event}' handler ` +
174174
`moved the write to another row. The engine now resolves the target BEFORE the before phase ` +
175175
`and computes the whole write against it — the pre-image, the 'readonlyWhen' locks, the ` +
176176
`validation rules — so a by-id target is immutable once a handler runs, on BOTH verbs. ` +
177-
`'delete()' honoured a rebind until #6752 by re-resolving the new target; that is retired ` +
178-
`too, so one rule now covers both.`
177+
`'delete()' used to honour a rebind by re-resolving the new target; that is retired ` +
178+
`too, because a handler that silently redirects which row gets deleted is a trap — so ` +
179+
`one rule now covers both.`
179180
: path === 'unscoped-multi'
180181
? ` This is the whole-operation dispatch an UNSCOPED predicate write delivers to a declared ` +
181-
`shape guard (#9719, both write verbs since #9974): its 'id' is present-but-undefined ON ` +
182+
`shape guard (one registered with 'dispatchUnscopedMultiWrite', on update and delete ` +
183+
`alike): its 'id' is present-but-undefined ON ` +
182184
`PURPOSE — there is no target row — and the dispatch ladder was resolved before any handler ` +
183185
`ran, so binding 'input.id' here retargets nothing. It is refused rather than ignored, ` +
184186
`because a silent no-op is the failure this contract exists to abolish.`

‎packages/objectql/src/integrity/dangling-reference-audit.test.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ describe('[#4551] dangling stored references are reported, never rewritten', ()
374374
});
375375
await auditDanglingReferences(dirty);
376376
expect(dirty.warnings).toHaveLength(1);
377-
expect(dirty.warnings[0][0]).toContain('#4551');
377+
expect(dirty.warnings[0][0]).toContain('reported, never rewritten');
378378
expect((dirty.warnings[0][1] as any).references).toEqual([
379379
'sys_position_permission_set#ppr_1.permission_set_id → sys_permission_set#ps_gone',
380380
]);
@@ -495,7 +495,7 @@ describe('[#4747] a run that was called off is not a finding about the data', ()
495495
expect(out.unreadableObjects).toEqual([]);
496496
// The real finding is still reported, and the summary line carries the
497497
// incompleteness so the log cannot read as a finished run either.
498-
const summary = port.warnings.find((w) => w[0].includes('#4551'));
498+
const summary = port.warnings.find((w) => w[0].includes('reported, never rewritten'));
499499
expect(summary).toBeDefined();
500500
expect((summary![1] as any).aborted).toBe(true);
501501
});
@@ -712,7 +712,7 @@ describe('[#4743] provenance references are audited, in their OWN bucket', () =>
712712

713713
const out = await auditDanglingReferences(port);
714714

715-
const summary = port.warnings.find((w) => w[0].includes('#4551'));
715+
const summary = port.warnings.find((w) => w[0].includes('reported, never rewritten'));
716716
expect(summary).toBeDefined();
717717
const meta = summary![1] as Record<string, unknown>;
718718
expect(meta.dangling).toBe(1);
@@ -956,7 +956,7 @@ describe('[#5718] objects a finite budget never reached are named, not dropped',
956956
});
957957
await auditDanglingReferences(loud, { maxRows: 1 });
958958

959-
const summary = loud.warnings.find((w) => w[0].includes('#4551'));
959+
const summary = loud.warnings.find((w) => w[0].includes('reported, never rewritten'));
960960
expect(summary).toBeDefined();
961961
const meta = summary![1] as Record<string, unknown>;
962962
// Itemised, not merely counted: object-scale, and a reader who has to act

‎packages/objectql/src/integrity/dangling-reference-audit.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -733,7 +733,7 @@ export async function auditDanglingReferences(
733733
// They ride along whenever the line fires for a real finding; the full
734734
// report always carries them for a caller that came looking.
735735
if (report.dangling.length || report.undetermined || report.unreadableObjects.length) {
736-
port.warn?.('[integrity] stored references that resolve to nothing (#4551)', {
736+
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', {
737737
scanned: report.scanned,
738738
dangling: report.dangling.length,
739739
undetermined: report.undetermined,

‎packages/objectql/src/lifecycle/lifecycle-settings.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export const lifecycleSettingsManifest = {
4646
'Per-object window overrides: { "<object>": { "maxAge": "1y", "expireAfter": "30d" } }. ' +
4747
'Duration literals: h/d/w/y. Tenant-scoped — a regulated tenant sets years while dev keeps days (ADR-0057 §3.2). ' +
4848
'An override BELOW a retention floor a consumer registered (e.g. the job queue\'s dedup window) is rejected at ' +
49-
'sweep time and logged at error — the declared window keeps running (#5195).',
49+
'sweep time and logged at error — the declared window keeps running.',
5050
},
5151
{
5252
type: 'json',

‎packages/objectql/src/plugin.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ export class ObjectQLPlugin implements Plugin {
495495
});
496496
this.subscribeMetadataRebind(ctx, protocolShim);
497497
} else {
498-
ctx.logger.info('registerProtocol=false — protocol assembly delegated to MetadataProtocolPlugin (ADR-0076 Step 2, #2462)');
498+
ctx.logger.info('registerProtocol=false — protocol assembly delegated to MetadataProtocolPlugin (ADR-0076 Step 2)');
499499
}
500500

501501
// ADR-0057: the platform-owned LifecycleService. Registered from the

0 commit comments

Comments
 (0)