Skip to content

Commit 4f85e4d

Browse files
claude[bot]claude
andauthored
fix(trigger-record-change): decouple the flow-facing record from the batch payload (#15475)
* wip(trigger-record-change): decouple flow record from batch payload + adopt #15356 harness Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ * test(trigger-record-change): flip S5/S5b and the SQL replica to cannot-reach pins Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ * test(trigger-record-change): seam pin for decoupling + expand-graft control Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ * chore(changeset): decouple flow record from batch payload Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6c9f34f commit 4f85e4d

5 files changed

Lines changed: 1294 additions & 6 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
"@objectstack/trigger-record-change": patch
3+
---
4+
5+
fix(trigger-record-change)!: the record handed to a record-change flow no longer aliases the write's payload (#14744)
6+
7+
<!-- adr-0087: not-required (no-migration-prescription) Nothing metadata-shaped moves: no spec key, no Zod schema, no `packages/spec` declaration, no export, no config field and no stored row changes spelling or shape, and a stored flow definition is byte-identical before and after — `objectstack migrate meta` has nothing to reach. What moves is the reference identity of the object one runtime seam hands a flow. The consumer note below names the supported node for writing a record; it prescribes no rewrite of any authored artifact, and the code it could affect is a stack author's own registered function body, which the metadata upgrader cannot see. -->
8+
9+
**BREAKING** for a flow whose `script` node mutates a NESTED value of the
10+
triggering record IN PLACE: that mutation no longer affects the write the flow
11+
was triggered by. Shipped as `patch` — this change moves no public surface (no
12+
exported symbol, no accepted key or value), and under the maintainer's
13+
2026-09-04 rule (decision batch #35, on #15294) a `fix(` that changes no public
14+
surface stays `patch`, with breaking-ness carried by this banner and the
15+
ADR-0087 disposition rather than by the level. Maintainer ruling 2026-09-04 on
16+
#14744 (decision batch #38, verbatim 「同意」), adopting option A.
17+
18+
**Why.** `buildContext` builds the flow's `record` as a shallow overlay of the
19+
pre-image, the mutation payload and the after-row. The top-level object was
20+
new, so a flow ASSIGNING a top-level key reached nothing — but every nested
21+
value in it was the engine's own object, shared by reference. One of those is
22+
`ctx.input.data`, and on a `multi: true` update ADR-0058 Addendum II D3 hands
23+
every per-row context that same payload object, which is the SET clause of the
24+
single `updateMany`. A registered function doing `record.tags.push(...)`
25+
therefore wrote the SET clause without assigning any key: every dispatch's
26+
contribution landed on EVERY matched row, including values derived from another
27+
row's pre-image, and #14099's key-set refusal could not see it because no key
28+
was assigned. Measured end to end on the memory driver and on
29+
`@objectstack/driver-sql` (#15356).
30+
31+
**What changes.** Both flow-facing roots — `record` (and the `params` alias of
32+
it) and `previous` — are decoupled from the engine's state before the flow
33+
runs. Arrays, plain objects, `Date`, `RegExp`, `Map` and `Set` are copied;
34+
primitives, functions and other class instances are shared, which is the
35+
documented and pinned boundary. A flow still mutates its roots freely and still
36+
observes its own writes for the rest of the run; those writes simply reach
37+
nothing outside it. `previous` is decoupled in the same stroke because it is the
38+
engine's single pre-image object and the same hook context reaches every other
39+
flow bound to the same write.
40+
41+
**What does NOT change.** The engine's write shape. ADR-0058 Addendum II D3
42+
stands untouched: one payload still serves N rows and every per-row context is
43+
still handed that one object. #14099's key-set refusal is untouched and is not
44+
widened — a hook that assigns the same key with per-row values still passes it,
45+
and divergent key sets are still refused whole. Flow metadata with no registered
46+
function reached nothing before this change and reaches nothing after it:
47+
assignment nodes write the run's variable map, and `update_record` issues its own
48+
by-id write. Lookup expansion (`config.expand`) still grafts onto the record the
49+
flow holds.
50+
51+
**Consumer note.** A flow that relied on an in-place nested mutation to persist
52+
— which on a by-id write did persist, and on a `multi: true` write corrupted
53+
every other matched row — writes the record with the `update_record` node
54+
instead. That node is the supported per-row write and is unaffected by this
55+
change.

0 commit comments

Comments
 (0)