Skip to content

Commit cb2bc18

Browse files
committed
docs(automation): un-stale the run summary line and narrow what failed=0 claims (#14456)
Contract review, three docs-only changes; no engine change. - `flows.mdx` printed the summary line as literal log output and was left stale by this PR's own token: the run it shows now prints `skipped=30 failed=0 gate=…`, and the line sits directly above the table row documenting `failed`. - Narrowed the reading of `failed=0` in three places — the doc's `failed` row, `formatRunSummaryLine`'s comment and the changeset — from "nothing failed" to what was measured: no node execution OF THIS RUN failed. A `subflow` child's contained failures stay on the child's summary rather than rolling up the way `acted` does. Cites #15617, where the declaration's two paragraphs are being reconciled. `content/docs/releases/v17.mdx` carries the same line and is deliberately NOT touched: release notes are a record of what that release printed, and are never edited from a code PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
1 parent 56eeff2 commit cb2bc18

3 files changed

Lines changed: 20 additions & 7 deletions

File tree

.changeset/contained-failure-visibility.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ A contained per-iteration failure is now visible at run level, attributed to its
99
Four changes populate the contract `@objectstack/spec` already declares:
1010

1111
- **`FlowRunSummary.failed`**`summarizeRun` now folds `failed = Σ nodes[].failures` over the per-node array it publishes, so the run-level count can never disagree with the breakdown it summarizes. It counts every node execution that failed, contained or fatal; on a run that completed, all of them were contained.
12-
- **`failed=N` on the run summary line**`formatRunSummaryLine` prints the token whenever the count is present, `failed=0` included. That is the opposite of the `unmeasured` rule beside it and deliberate: `unmeasured` qualifies `acted`, while `failed` answers a question a completed run's line otherwise cannot be asked at all.
12+
- **`failed=N` on the run summary line**`formatRunSummaryLine` prints the token whenever the count is present, `failed=0` included. That is the opposite of the `unmeasured` rule beside it and deliberate: `unmeasured` qualifies `acted`, while `failed` answers a question a completed run's line otherwise cannot be asked at all. Read `failed=0` precisely: **no node execution of this run failed**. It is the node fold and only that, so a `subflow` child's own contained failures stay on the child's summary rather than rolling up the way `acted` does — see #15617, where the declaration's two paragraphs are being reconciled.
1313
- **Iteration through `try_catch`** — a step that ran in a `try` or `catch` region inside a loop body now carries the enclosing loop's `iteration`, with `regionKind` still `try` / `catch`. The step says which region ran it *and* which row it ran for. `parallel` branch tagging is unchanged.
1414
- **`$error` binds the row** — the value bound to `errorVariable` (default `$error`) is the declared `TryCatchErrorValue`: `nodeId` and `message` as before, plus `iteration` and the loop's current `item` when the failure happened inside a loop body. A `subflow` / `map` child run has its own variable scope and therefore binds neither, so a parent's row identity never leaks into a child's `$error`.
1515

content/docs/automation/flows.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -872,7 +872,7 @@ terminal run therefore carries a **summary** — on the `AutomationResult`, on t
872872
run in `listRuns` / `getRun`, and in the log:
873873

874874
```
875-
[automation] run flow=stalled_deal_sweep run=run_a1b2 status=completed durationMs=142 selected=30 acted=0 skipped=30 gate=check_stalled->send_nudge:30
875+
[automation] run flow=stalled_deal_sweep run=run_a1b2 status=completed durationMs=142 selected=30 acted=0 skipped=30 failed=0 gate=check_stalled->send_nudge:30
876876
```
877877

878878
| Field | Meaning |
@@ -881,7 +881,7 @@ run in `listRuns` / `getRun`, and in the log:
881881
| `acted` | Records **created / updated / deleted**, plus effects dispatched (notifications delivered) |
882882
| `skipped` | Node executions a **closed gate** prevented — one per loop iteration whose conditional edge evaluated false |
883883
| `unmeasured` | Executions that reached something the platform **cannot count** — see below |
884-
| `failed` | Node executions that **failed and were contained** caught by a `try_catch` or routed down a `fault` edgeso the run went on; the sum of `nodes[].failures`. Absent on a run that did not track it, which is not zero |
884+
| `failed` | Node executions **of this run** that failed — on a completed run every one of them was contained, caught by a `try_catch` or routed down a `fault` edge, so the run went on; the sum of `nodes[].failures`. `failed=0` therefore reads "no node execution of this run failed", which is narrower than "nothing failed anywhere": a `subflow` child's own contained failures are counted on the CHILD's summary, not folded up here the way `acted` is (that inconsistency in the declaration is [#15617](https://github.com/objectstack-ai/objectstack/issues/15617)). Absent on a run that did not track it, which is not zero |
885885
| `nodes[]` | Per-node terminal status with `runs` / `failures` / `skipped` and its own selected/acted |
886886
| `gates[]` | Which gates closed and how often, most-skipped first |
887887

packages/services/service-automation/src/run-summary.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,23 @@ export function formatRunSummaryLine(
199199
// `unmeasured` is a qualifier on `acted`: absent, the zero beside it is
200200
// trustworthy. `failed` answers a question the line otherwise cannot be
201201
// asked at all — a completed run says nothing about the rows it lost — so
202-
// the token has to be there to be read, and `failed=0` is the reading
203-
// "nothing failed" as against a line with no token at all, which is the
204-
// older "not tracked". A run summarized by `summarizeRun` always carries
205-
// it; only a summary persisted before this existed prints nothing here.
202+
// the token has to be there to be read.
203+
//
204+
// What `failed=0` says, exactly: NO NODE EXECUTION OF THIS RUN FAILED.
205+
// That is narrower than "nothing failed", and the difference is a
206+
// `subflow`: the fold this prints is `Sigma nodes[].failures` over THIS
207+
// run's own nodes, so a child run that CONTAINED failures of its own
208+
// reports them on the child's summary and the parent still prints
209+
// `failed=0` — measured, alongside the control where a child that FAILS
210+
// rather than contains does reach the parent's count through the
211+
// `subflow` node's own failure step. `acted` rolls a child's totals up
212+
// and this does not; the declaration says both things in two paragraphs
213+
// and is being reconciled in #15617. Until it is, this line is the node
214+
// fold, and only that.
215+
//
216+
// A line with no token at all is a different reading again — the older
217+
// "not tracked". A run summarized by `summarizeRun` always carries the
218+
// count; only a summary persisted before this existed prints nothing here.
206219
if (summary.failed !== undefined) parts.push(`failed=${summary.failed}`);
207220
if (summary.unmeasured) parts.push(`unmeasured=${summary.unmeasured}`);
208221
const topGate = summary.gates[0];

0 commit comments

Comments
 (0)