From 0266c505018af69be639c534f795eb450a266b8a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 09:20:59 +0000 Subject: [PATCH 1/3] docs(objectql): state the #16074 per-row `previous` rule in the published docblock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `dispatchPerRowBeforeHooks`'s docblock and one test comment still stated the RETIRED rule — per-row `previous` supplied "so a guard can REFUSE the write (throw), not so a rewrite can be aimed". Ruling #16074 (landed in `@objectstack/spec` by PR #17249) admits a row-invariant-in-effect rewrite: the same written key set on every matched row, assigned IN PLACE, kept safe by the engine's `MULTI_UPDATE_HOOK_KEY_DIVERGENCE` refusal. Key-set divergence, a per-row value and a row-conditioned REPLACEMENT of `ctx.input.data` stay out of contract. Prose only; the runtime already follows the new rule. The docblock is published surface — the retired sentence ships in six files of the `@objectstack/objectql@17.4.0` tarball, including `dist/util-Dw5ZTIII.d.ts:3554` on a member of the exported `ObjectQL` class — so a changeset is owed. Claude-Session: https://claude.ai/code/session_01CqmCgU5RGDoJYhHUMVp2af Co-authored-by: Claude --- ...7975-objectql-per-row-previous-docblock.md | 30 +++++++++++++++++++ .../src/bulk-write-per-row-hooks.test.ts | 8 +++-- packages/objectql/src/engine.ts | 13 +++++--- 3 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 .changeset/17975-objectql-per-row-previous-docblock.md diff --git a/.changeset/17975-objectql-per-row-previous-docblock.md b/.changeset/17975-objectql-per-row-previous-docblock.md new file mode 100644 index 00000000000..e85b8571b22 --- /dev/null +++ b/.changeset/17975-objectql-per-row-previous-docblock.md @@ -0,0 +1,30 @@ +--- +'@objectstack/objectql': patch +--- + +docs(objectql): the per-row `before*` docblock states the #16074 rule — a row-invariant-in-effect rewrite is ADMITTED (#17975) + +`dispatchPerRowBeforeHooks`'s docblock (ADR-0058 Addendum II, clause D3) still +said per-row `previous` was supplied *"so a guard can REFUSE the write (throw), +not so a rewrite can be aimed"*, and a test comment in +`bulk-write-per-row-hooks.test.ts` said the same. Ruling #16074, landed in +`@objectstack/spec` by PR #17249, retired that: a per-row `previous`-conditioned +rewrite is admitted when its written KEY SET is the same on every matched row +and is assigned IN PLACE, kept safe by the engine's +`MULTI_UPDATE_HOOK_KEY_DIVERGENCE` refusal (#14099). Key-set divergence, a +per-row VALUE and a row-conditioned REPLACEMENT of `ctx.input.data` all stay +outside the contract. + +This is published text, not an internal comment: JSDoc on a `private` member +survives `.d.ts` emit. Measured in the shipped `@objectstack/objectql@17.4.0` +tarball — the retired sentence is present in six published files, including +`dist/util-Dw5ZTIII.d.ts:3554`, on a member of the `ObjectQL` class that both +the `.` and `./core` entrypoints export. Every consumer's editor surfaces it on +hover, so as soon as spec's changeset is consumed the two packages would state +opposite contracts. + +No behaviour change: the engine already follows the new rule, and the three +shipped provenance stamps (`email-template-provenance.ts`, +`sharing-rule-provenance.ts`, `webhook-provenance.ts`) all assign in place. The +admitted shape's coverage already exists in +`multi-update-hook-key-divergence.test.ts`; the test comment now points at it. diff --git a/packages/objectql/src/bulk-write-per-row-hooks.test.ts b/packages/objectql/src/bulk-write-per-row-hooks.test.ts index 8a668c68c30..236be89bfcc 100644 --- a/packages/objectql/src/bulk-write-per-row-hooks.test.ts +++ b/packages/objectql/src/bulk-write-per-row-hooks.test.ts @@ -566,8 +566,12 @@ describe('[#5574 / D2] the per-row before context is the SINGLE-RECORD shape', ( }); it('a `previous`-reading GUARD can now refuse a bulk write per row', async () => { - // What per-row `previous` is FOR (D3 says so in as many words): so a guard - // can REFUSE, not so a rewrite can be aimed at one row. + // ONE of the two things per-row `previous` is FOR (D3 says so in as many + // words): so a guard can REFUSE. The other, ruled on #16074, is a + // ROW-INVARIANT-IN-EFFECT rewrite — the same written key set on every + // matched row, assigned in place — which + // `multi-update-hook-key-divergence.test.ts` covers. What stays outside + // the contract is a rewrite AIMED at one row. const { engine } = await boot([hook('guard', 'beforeUpdate', (ctx) => { if ((ctx.previous as any)?.status === 'locked') throw new Error('row is locked'); })]); diff --git a/packages/objectql/src/engine.ts b/packages/objectql/src/engine.ts index aa6ec36f32e..edf4a5eec5c 100644 --- a/packages/objectql/src/engine.ts +++ b/packages/objectql/src/engine.ts @@ -3445,10 +3445,15 @@ export class ObjectQL implements IObjectQLEngine { * its dispatch — that write-back is what makes "accumulate in dispatch order" * true for both spellings rather than only the first. * - * A rewrite CONDITIONED on the row (`ctx.previous`, `ctx.input.id`) is - * outside the contract: it does not scope itself to the row it was decided - * on, it widens to every matched row. Per-row `previous` is supplied so a - * guard can REFUSE the write (throw), not so a rewrite can be aimed. + * A rewrite CONDITIONED on the row (`ctx.previous`, `ctx.input.id`) cannot + * scope itself to the row it was decided on: it widens to every matched row. + * Per-row `previous` is supplied so a guard can REFUSE the write (throw), + * and — ruled on #16074 — so a hook can make a ROW-INVARIANT-IN-EFFECT + * rewrite: one whose written KEY SET is the same on every matched row AND is + * assigned IN PLACE (`ctx.input.data.x = 1`, never `ctx.input.data = {…}`, + * which the recording below cannot attribute). A rewrite AIMED at one row + * stays outside the contract, and what makes the admitted shape safe is that + * enforcement rather than the hook's good faith. * * ## D3, ENFORCED — divergent key sets refuse the batch [#14099] * From cd220040482a8d5f0b64a19932e2926a79e1ca14 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 09:27:33 +0000 Subject: [PATCH 2/3] =?UTF-8?q?chore(changeset):=20grade=20the=20objectql?= =?UTF-8?q?=20docblock=20repair,=20declared=20clause=20=E2=91=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docblock is published text (it ships in `dist/util-*.d.ts`), so a changeset is owed. Graded `minor` because this PR declares clause ②: the level axis in `check-changeset-no-major.mjs` refuses a declared clause ② whose every moved package is graded `patch`. Claude-Session: https://claude.ai/code/session_01CqmCgU5RGDoJYhHUMVp2af Co-authored-by: Claude --- .changeset/17975-objectql-per-row-previous-docblock.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.changeset/17975-objectql-per-row-previous-docblock.md b/.changeset/17975-objectql-per-row-previous-docblock.md index e85b8571b22..e05cd5037aa 100644 --- a/.changeset/17975-objectql-per-row-previous-docblock.md +++ b/.changeset/17975-objectql-per-row-previous-docblock.md @@ -1,5 +1,5 @@ --- -'@objectstack/objectql': patch +'@objectstack/objectql': minor --- docs(objectql): the per-row `before*` docblock states the #16074 rule — a row-invariant-in-effect rewrite is ADMITTED (#17975) @@ -28,3 +28,9 @@ shipped provenance stamps (`email-template-provenance.ts`, `sharing-rule-provenance.ts`, `webhook-provenance.ts`) all assign in place. The admitted shape's coverage already exists in `multi-update-hook-key-divergence.test.ts`; the test comment now points at it. + +Graded `minor`, not `patch`: this PR declares clause ② (the docblock it moves is +published contract text), and `check-changeset-no-major`'s level axis refuses a +declared clause ② whose every moved package is graded `patch`. The act itself +adds no exported symbol, key or accepted value — the accept set moved in +`@objectstack/spec` under PR #17249, not here. From c3de704e180a25c7569f26d64c1aac574f2976e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 09:53:32 +0000 Subject: [PATCH 3/3] =?UTF-8?q?chore(changeset):=20regrade=20the=20docbloc?= =?UTF-8?q?k=20repair=20`patch`=20=E2=80=94=20no=20clause=20=E2=91=A1=20is?= =?UTF-8?q?=20declared?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The seat overturned its own `Clause-②: yes` grading on #17975 and posted the correction: this card adds no exported symbol, no key and no accepted value, so it widens nothing. The widening was PR #17249's, in `@objectstack/spec`. The `yes` had leaned on a precedent that moved exported SYMBOLS through a barrel; this moves prose, and the four in-repo precedents for a prose repair inside a published package are all `patch`. With no clause ② declared, `check-changeset-no-major`'s level axis stands down, which is what made the `minor` grade necessary in the first place. Claude-Session: https://claude.ai/code/session_01CqmCgU5RGDoJYhHUMVp2af Co-authored-by: Claude --- .changeset/17975-objectql-per-row-previous-docblock.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.changeset/17975-objectql-per-row-previous-docblock.md b/.changeset/17975-objectql-per-row-previous-docblock.md index e05cd5037aa..2051b99186b 100644 --- a/.changeset/17975-objectql-per-row-previous-docblock.md +++ b/.changeset/17975-objectql-per-row-previous-docblock.md @@ -1,5 +1,5 @@ --- -'@objectstack/objectql': minor +'@objectstack/objectql': patch --- docs(objectql): the per-row `before*` docblock states the #16074 rule — a row-invariant-in-effect rewrite is ADMITTED (#17975) @@ -29,8 +29,6 @@ shipped provenance stamps (`email-template-provenance.ts`, admitted shape's coverage already exists in `multi-update-hook-key-divergence.test.ts`; the test comment now points at it. -Graded `minor`, not `patch`: this PR declares clause ② (the docblock it moves is -published contract text), and `check-changeset-no-major`'s level axis refuses a -declared clause ② whose every moved package is graded `patch`. The act itself -adds no exported symbol, key or accepted value — the accept set moved in -`@objectstack/spec` under PR #17249, not here. +Graded `patch`: the act moves published PROSE. It adds no exported symbol, no +key and no accepted value — the accept set was widened by PR #17249 in +`@objectstack/spec`, not here — so this PR declares no clause ②.