You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
service-automation: populate the contained-failure visibility contract — FlowRunSummary.failed fold, loop iteration through try_catch → runRegion, $error.iteration / $error.item, failed= on the summary line (engine half of #13681) #14456
Part of #13681 — the engine half of the ruled B-branch's visibility rider, filed into the domain:services queue by the domain:spec seat (session session_01GDA48PuRFrHyRfdkBz8m21) under the three-surface split the retriage prescribed (comment 5479171811: spec half first, engine half behind it). The contract this card implements is declared by PR #14452 (spec half, ACCEPT + Clause-② PASS, comment 5506108981); the lint/docs limbs are #14394.
Reader: the domain:services execution seat, at its next selection pass once the blocker is merged (plain pm:queue candidate; triage adds domain:* and type). Unlock predicate: PR #14452 merged to main — verify with git grep -n "TryCatchErrorValueSchema" origin/main -- packages/spec/src/automation/control-flow.zod.ts (1+ hits) before dispatch.
Ruling of record (do not re-decide)
Maintainer 2026-08-31 (director batch #18, verbatim 「其他同意」) on the conditional ruling (issue comment 5478768627); branch B selected by measurement (comment 5478879587): ⛔ no loop.config.onIterationError key — loop { body: [ try_catch { try, catch } ] } is the containment spelling. The rider binds the visibility half to #13681 (⛔ not a low-priority orphan): a caught per-iteration failure must be visible at run level, attributable to its iteration, and bound to its row.
Exact contract to populate (from PR #14452's "Handoff to the engine card", verbatim in substance)
FlowRunSummary.failed — summarizeRun sets failed = Σ node.failures over nodes (every failure step, contained or fatal); persist it with the run row. Older rows keep it absent; ⛔ never default to 0 (absent is "not tracked", the unmeasured convention).
Per node — no new key; keep incrementing FlowRunNodeSummary.failures per failure step exactly as today (the spec seat ruled option A: one counter per fact).
$error binding — try-catch-node.ts binds a TryCatchErrorValue (import the schema/type from @objectstack/spec's automation entry): nodeId, message, plus iteration and item (the enclosing loop's iteratorVariable value) only when inside a loop body; both absent outside a loop.
formatRunSummaryLine prints failed=N when summary.failed is present (present-and-zero prints failed=0; absent prints nothing).
Acceptance (executable)
The 5-row / third-fails measurement from comment 5478851960 (loop { body: [ try_catch { try: [notify], catch: [assignment] } ] } on the real AutomationEngine, 5 elements, element 3 fails) reproduced as a test in packages/services/service-automation: run status completed, 5/5 iterations, summary.failed === 1 (or 2 for the two-caught variant the measurement also ran), the catch region's step carrying iteration: 2 with regionKind: 'catch', and $error.iteration === 2 / $error.item equal to the failing row.
formatRunSummaryLine snapshot with failed= present, and with an older summary (no failed) printing nothing for it.
Rows persisted before this change parse with failed absent (no migration, no default).
Part of #13681 — the engine half of the ruled B-branch's visibility rider, filed into the
domain:servicesqueue by thedomain:specseat (sessionsession_01GDA48PuRFrHyRfdkBz8m21) under the three-surface split the retriage prescribed (comment 5479171811: spec half first, engine half behind it). The contract this card implements is declared by PR #14452 (spec half, ACCEPT + Clause-② PASS, comment 5506108981); the lint/docs limbs are #14394.Blocked-by: #14452
Reader: the
domain:servicesexecution seat, at its next selection pass once the blocker is merged (plainpm:queuecandidate; triage addsdomain:*and type). Unlock predicate: PR #14452 merged tomain— verify withgit grep -n "TryCatchErrorValueSchema" origin/main -- packages/spec/src/automation/control-flow.zod.ts(1+ hits) before dispatch.Ruling of record (do not re-decide)
Maintainer 2026-08-31 (director batch #18, verbatim 「其他同意」) on the conditional ruling (issue comment 5478768627); branch B selected by measurement (comment 5478879587): ⛔ no
loop.config.onIterationErrorkey —loop { body: [ try_catch { try, catch } ] }is the containment spelling. The rider binds the visibility half to #13681 (⛔ not a low-priority orphan): a caught per-iteration failure must be visible at run level, attributable to its iteration, and bound to its row.Exact contract to populate (from PR #14452's "Handoff to the engine card", verbatim in substance)
FlowRunSummary.failed—summarizeRunsetsfailed = Σ node.failuresovernodes(everyfailurestep, contained or fatal); persist it with the run row. Older rows keep it absent; ⛔ never default to0(absent is "not tracked", theunmeasuredconvention).FlowRunNodeSummary.failuresperfailurestep exactly as today (the spec seat ruled option A: one counter per fact).try-catch-node.tspasses the enclosing loop's iteration intorunRegion's grouping: a step insidetry/catchinside a loop body must carryiteration: <loop index>withregionKindstill'try' | 'catch'. TodayrunRegiononly fills fields the innermost tagger left undefined, so either the try/catch call site forwards the loop'siterationor the tagger fillsiterationon already-tagged steps that have none — pick the one that keepsparallelbranches untouched (see Aparallelbranch inside aloopbody overloads the step record'siterationwith the branch index — the enclosing loop iteration is lost, so a branch step cannot be attributed to its row #14414 for the adjacentparallel-in-loopoverload; ⛔ do not fold it in).$errorbinding —try-catch-node.tsbinds aTryCatchErrorValue(import the schema/type from@objectstack/spec's automation entry):nodeId,message, plusiterationanditem(the enclosing loop'siteratorVariablevalue) only when inside a loop body; both absent outside a loop.formatRunSummaryLineprintsfailed=Nwhensummary.failedis present (present-and-zero printsfailed=0; absent prints nothing).Acceptance (executable)
loop { body: [ try_catch { try: [notify], catch: [assignment] } ] }on the realAutomationEngine, 5 elements, element 3 fails) reproduced as a test inpackages/services/service-automation: run statuscompleted, 5/5 iterations,summary.failed === 1(or2for the two-caught variant the measurement also ran), the catch region's step carryingiteration: 2withregionKind: 'catch', and$error.iteration === 2/$error.itemequal to the failing row.formatRunSummaryLinesnapshot withfailed=present, and with an older summary (nofailed) printing nothing for it.failedabsent (no migration, no default).Out of scope
parallelinsideloopiteration attribution (Aparallelbranch inside aloopbody overloads the step record'siterationwith the branch index — the enclosing loop iteration is lost, so a branch step cannot be attributed to its row #14414, decision card).#13803(summary reportsacted: 0after a mid-loop death) — independent, ⛔ do not merge.loopbody with a fallible node and no containment, and atry_catchwithoutcatch; documentloop { try_catch }as the per-iteration containment spelling (B-branch items 1–2 of #13681) #14394).Generated by Claude Code