|
| 1 | +--- |
| 2 | +'@objectstack/spec': minor |
| 3 | +--- |
| 4 | + |
| 5 | +feat(spec): `ExecutionStepMetrics` gains an optional `failures` slot, and `FlowRunSummary.failed` is declared as the fold INCLUDING what a delegating node rolled up from its child (maintainer ruling 2026-09-06 on #15617, spec half) |
| 6 | + |
| 7 | +Additive. Nothing an author writes is renamed, retired or narrowed; no accept |
| 8 | +set shrinks. One optional key is declared on a runtime-produced schema and the |
| 9 | +prose of a published contract is reconciled with itself. |
| 10 | + |
| 11 | +**What was wrong.** `FlowRunSummary` said two things about `failed`. Its |
| 12 | +header paragraph declared that a `subflow` node rolls its child run's totals |
| 13 | +up into the parent — "this summary answers *what did this run cause*" — while |
| 14 | +the field itself declared `failed = Σ nodes[].failures`, a fold over the |
| 15 | +parent's own node executions. For a parent that delegates its rows to a |
| 16 | +`subflow` (or a `map` item) those give different answers, and the engine could |
| 17 | +only satisfy the second one: `ExecutionStepMetrics` carried `selected` / |
| 18 | +`acted` / `unmeasuredEffect` and no failure slot, so a child's contained |
| 19 | +failures had no path into the parent's fold. Measured on the real engine by |
| 20 | +the services seat (#15617): parent `loop { subflow(child) }` → parent |
| 21 | +`failed=0` while the five child summaries carried `failed=[0,0,0,0,1]` — |
| 22 | +`acted` rolled up, `failed` did not. |
| 23 | + |
| 24 | +**What this declares.** |
| 25 | + |
| 26 | +- `ExecutionStepMetrics.failures` (optional, integer ≥ 0): node executions |
| 27 | + that failed inside a child run this execution delegated to and went on from |
| 28 | + — a `subflow` child or a `map` item whose run COMPLETED while containing |
| 29 | + failures, i.e. the child's `summary.failed`, rolled up. It folds into the |
| 30 | + delegating node's `nodes[].failures` and so into the run-level `failed` — |
| 31 | + the same fold shape `acted` has, but not the same rule at the failed-child |
| 32 | + boundary (next bullet). Absent means the step delegated nothing, or its |
| 33 | + child tracked no count, or the producer did not track it (every step the |
| 34 | + engine emits between this release and the engine half) — never zero. |
| 35 | +- It is NOT the step's own outcome. A step that failed is `status: 'failure'` |
| 36 | + and counts once through `nodes[].failures`, as before; a child that FAILED |
| 37 | + — whether or not it also contained failures before it failed — is |
| 38 | + precisely that step failure: its own `failed`, contained and fatal alike, |
| 39 | + stays on the child's run row and nothing rides up, so one failure is never |
| 40 | + counted twice. This is where the rule parts from `acted`, which does carry |
| 41 | + a failed child's writes up to the parent. The control the card measured (a |
| 42 | + failing child → parent `failed=1`) keeps counting exactly as today. |
| 43 | +- `FlowRunSummary.failed` is declared, at the field, as the fold of |
| 44 | + `nodes[].failures` INCLUDING what a delegating node rolled up; the |
| 45 | + `FlowRunNodeSummary.failures` describe names the roll-up path, and its |
| 46 | + `status` describe states that a delegating node whose child contained |
| 47 | + failures reads `success` beside `failures > 0` — status is judged on the |
| 48 | + node's own executions. |
| 49 | + |
| 50 | +**What this does not do yet.** This is the contract half of a two-lane |
| 51 | +landing (contract first). No producer populates `failures` in this release: |
| 52 | +`subflow-node.ts` and the `map` node roll the child's contained failures into |
| 53 | +the slot in the services half, #16314, and only then does a parent's |
| 54 | +`failed` start counting them. Until that lands, every `ExecutionStepMetrics` |
| 55 | +the engine emits is byte-identical to today's, `failed` is numerically what it |
| 56 | +was, and the flow-run reference page keeps the narrowed wording PR #15609 |
| 57 | +shipped ("node executions **of this run**") on purpose — it is widened when |
| 58 | +both halves are in. |
| 59 | + |
| 60 | +**Consumers.** A reader of `ExecutionStepMetrics` sees one more optional |
| 61 | +number and nothing else changes shape; a consumer that already sums |
| 62 | +`nodes[].failures` to cross-check `failed` keeps agreeing with it, because the |
| 63 | +fold is unchanged — the roll-up enters the per-node array, not beside it. Two |
| 64 | +consequences of that placement are part of the contract from this release, |
| 65 | +even though no producer populates the slot yet: on a delegating node |
| 66 | +`nodes[].failures` may exceed `runs` (`runs: 5, failures: 15` is a legal |
| 67 | +shape — five subflow executions whose children each contained three), and it |
| 68 | +is no longer only that node's own failed executions, so a reader that derived |
| 69 | +"this node's executions that failed" or a failure RATE from `failures / runs` |
| 70 | +must read a delegating node's number as "failures this node caused, its |
| 71 | +child's contained ones included". |
0 commit comments