Skip to content

Commit d30ccb9

Browse files
os-warrenclaude
andauthored
service-automation: populate the contained-failure visibility contract — FlowRunSummary.failed fold, loop iteration through try_catch, $error.iteration / $error.item, failed= on the summary line (#15609)
* feat(service-automation): populate the contained-failure visibility contract (#14456) `loop { body: [ try_catch { try, catch } ] }` contained a per-iteration failure but reported nothing about it: no run-level count, no iteration on the failing step, no row identity on `$error`. - `summarizeRun` folds `failed = Sigma nodes[].failures` over the array it publishes, so the run-level count cannot disagree with the breakdown. - `formatRunSummaryLine` prints `failed=N` whenever present, `failed=0` included; absent (an older row) prints nothing. Absent is "not tracked", never zero — no migration, no default. - `try_catch` forwards the enclosing loop's iteration into `runRegion`'s grouping for both regions, leaving the tagger and `parallel` untouched. - `$error` is the declared `TryCatchErrorValue`, gaining `iteration` and `item` inside a loop body and binding neither outside one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y * chore(service-automation): drop an unused test import (#14456) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y * test(service-automation): share one engine double across the run-history pins (#14456) `check:engine-double-contract` counts unguarded doubles PER FILE against a shrink-only baseline. Three copies of the same four-member fake would have been two new ledger rows for one fact; one shared factory keeps the file's population at the 5 the baseline already records. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y * docs(service-automation): name the filed spec gap (#14954) beside the `code` widening Refs #14456 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y * 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 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 236f2df commit d30ccb9

10 files changed

Lines changed: 699 additions & 21 deletions

File tree

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
---
2+
"@objectstack/service-automation": minor
3+
---
4+
5+
A contained per-iteration failure is now visible at run level, attributed to its iteration, and bound to its row.
6+
7+
`loop { body: [ try_catch { try, catch } ] }` is the containment spelling for a per-iteration failure that must not end the sweep (there is deliberately no `loop.config.onIterationError` key). Containment already worked — the failure was caught, the loop went on and the run completed — but nothing said what it had contained: a sweep that lost two rows out of five reported `status=completed selected=5 acted=9 skipped=0` and was indistinguishable from one that lost none. The failure was in the step log and in `nodes[].failures`; no run-level number carried it, the failing step named no row, and `$error` bound no row identity.
8+
9+
Four changes populate the contract `@objectstack/spec` already declares:
10+
11+
- **`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. 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.
13+
- **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.
14+
- **`$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`.
15+
16+
**`failed` absent means "not tracked", never `0`.** Runs recorded before this change keep it absent — no migration and no default, the same convention `unmeasured` carries. Defaulting it to zero would tell an operator "nothing failed" about a run nobody measured. Absent, the summary line prints no `failed=` token at all; present-and-zero prints `failed=0`. The count rides in the persisted `summary_json`, including on a summary compacted past the size cap, where the per-node `failures` it folds are exactly what gets dropped.

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

0 commit comments

Comments
 (0)