|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +import type { SemanticMigration } from '../../types.js'; |
| 4 | + |
| 5 | +export const entry: SemanticMigration = { |
| 6 | + id: 'execution-step-iteration-single-valued', |
| 7 | + surface: |
| 8 | + '`ExecutionStepLog.iteration` on a step whose `regionKind` is ' |
| 9 | + + '`parallel-branch` — the per-step records under `ExecutionLog.steps`, as ' |
| 10 | + + 'the automation run endpoints return them — and the new optional ' |
| 11 | + + '`ExecutionStepLog.branch` key', |
| 12 | + replacement: |
| 13 | + 'Read the parallel branch index from `branch`. `iteration` is now ' |
| 14 | + + 'single-valued: the zero-based iteration of the enclosing `loop`, carried ' |
| 15 | + + 'through any nesting, so a branch step of a `parallel` node that sits ' |
| 16 | + + 'inside a loop body carries BOTH keys — `iteration` for the row and ' |
| 17 | + + '`branch` for the branch. A consumer that grouped or labelled steps by ' |
| 18 | + + '`iteration` under `regionKind: parallel-branch` moves that read to ' |
| 19 | + + '`branch`; a consumer reading `iteration` on `loop-body`, `try` or ' |
| 20 | + + '`catch` steps changes nothing.', |
| 21 | + reason: |
| 22 | + 'The key was declared as the zero-based loop iteration OR the parallel ' |
| 23 | + + 'branch index of the enclosing region — one field, two meanings, told ' |
| 24 | + + 'apart only by reading `regionKind` first. The engine tagged each step ' |
| 25 | + + 'with its innermost region only, so for a `parallel` node inside a `loop` ' |
| 26 | + + 'body every branch step recorded the branch index and no step of that ' |
| 27 | + + 'branch recorded the loop iteration: a per-row failure inside a branch ' |
| 28 | + + 'was attributable to a branch, never to the row the sweep was processing. ' |
| 29 | + + 'The sibling try/catch rule had already settled the containment case — a ' |
| 30 | + + 'try/catch region has no index of its own, so it carries the loop ' |
| 31 | + + 'iteration — and deliberately left `parallel` open, because there the ' |
| 32 | + + 'two indexes genuinely compete for one field. The maintainer ruling of ' |
| 33 | + + '2026-09-03 took option A: `iteration` always means the enclosing loop ' |
| 34 | + + 'iteration and the branch index moves to its own optional key, so a ' |
| 35 | + + 'reader no longer has to branch on `regionKind` to know which number it ' |
| 36 | + + 'holds, and getting that wrong no longer silently books a failure against ' |
| 37 | + + 'the wrong row. Option B — keep the overload and add a second index whose ' |
| 38 | + + 'presence depends on nesting shape — was not taken. This is not a ' |
| 39 | + + 'mechanical conversion: a step record written before this change carries ' |
| 40 | + + '`iteration` under `parallel-branch` with the branch-index meaning, and ' |
| 41 | + + 'only its producer knows whether the parallel node sat inside a loop. The ' |
| 42 | + + 'measured corpus held zero `loop { parallel }` nestings and one consumer ' |
| 43 | + + 'reading the key — a grouping key in the objectui flow-runs panel — so ' |
| 44 | + + 'the migration is a consumer-side read move, not a data rewrite. The ' |
| 45 | + + 'engine tagger that writes both keys follows this contract change as its ' |
| 46 | + + 'own card; until it lands, `branch` is declared and unwritten, and ' |
| 47 | + + '`iteration` on a `parallel-branch` step written by an older engine still ' |
| 48 | + + 'holds the branch index.', |
| 49 | + acceptanceCriteria: |
| 50 | + 'No consumer reads `iteration` as a branch index: every read of a ' |
| 51 | + + '`parallel-branch` step\'s index goes through `branch`, and every read of ' |
| 52 | + + 'the enclosing loop iteration goes through `iteration` regardless of ' |
| 53 | + + '`regionKind`. A step record carrying `regionKind: parallel-branch`, ' |
| 54 | + + '`iteration: 3`, `branch: 1` parses under `ExecutionStepLogSchema` with ' |
| 55 | + + 'both numbers intact, and a negative or fractional `branch` is refused at ' |
| 56 | + + 'the `branch` path. A record written before the engine follow-on carries ' |
| 57 | + + 'no `branch` key; treat its `iteration` under `parallel-branch` as the ' |
| 58 | + + 'legacy branch index only when the record predates the engine build that ' |
| 59 | + + 'writes `branch`.', |
| 60 | +}; |
0 commit comments