feat(commons): FlowGraphBuilder folds outbound lifecycles into one node; async/spawn edges; Response from the inbound execution

Fold rule: outbound rows sharing (kind, CorrelationId) become one FlowStep with
a Lifecycle list — first row's identity/time/input, latest row's outcome/
subtitle/output/error, DurationMs = sync row's own or submit→terminal wall time
(null while pending). Chain skips async steps (Notify, or first row
CachedSubmit) and links them with Async edges from the preceding chain step.
Spawner edges from a non-RouteCall are Spawn: true. The Response links from the
last non-async step of the inbound execution (the RouteCall), not the globally
last cascaded step (D9). FlowGraphView places a Response at max column + 1 so
the D9 origin does not pull it left (pulled forward from Task 7 to keep the
layout test green).

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
Joseph Doherty
2026-09-06 01:42:48 -04:00
parent e26616bebc
commit 29129ef17a
5 changed files with 332 additions and 23 deletions
@@ -396,6 +396,14 @@ public partial class FlowGraphView : ComponentBase
}
}
// The Response is the flow's final node: it sits right of everything real regardless of
// its predecessor's column (the builder links it from the route call — D9 — which would
// otherwise drag it back to column 2 while the site work sits to its right).
var isResponse = (stepByKey.TryGetValue(k, out var sk) && sk.Kind == FlowStepKind.Response)
|| (predByKey.TryGetValue(k, out var pk) && pk.Kind == FlowStepKind.Response);
if (isResponse && column.Count > 0)
col = Math.Max(col, column.Values.Max() + 1);
while (!occupied.Add((lane[k], col)))
col++;
column[k] = col;