feat(commons): Flow Debugger outbound kinds, External lane, Pending/Parked outcomes, FlowLifecycleRow

FlowLane.External (3); FlowStepKind.External replaced by ApiCall/DbCall/Notify;
FlowStepOutcome gains Pending/Parked; new FlowLifecycleRow + additive
FlowStep.Lifecycle; FlowEdge gains Async/Spawn (default false). Predictor and
graph-view references renamed compile-only (full work in Tasks 6/7).

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
Joseph Doherty
2026-09-06 01:37:29 -04:00
parent 3a933e3f68
commit cc1bbf3975
8 changed files with 99 additions and 15 deletions
@@ -185,7 +185,9 @@ public partial class FlowGraphView : ComponentBase
FlowStepKind.AttributeWrite => "WRITE",
FlowStepKind.AttributeWait => "WAIT",
FlowStepKind.Response => "RESPONSE",
FlowStepKind.External => "EXTERNAL",
FlowStepKind.ApiCall => "API CALL",
FlowStepKind.DbCall => "DATABASE",
FlowStepKind.Notify => "NOTIFY",
_ => "STEP",
};
@@ -5,7 +5,9 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
/// <param name="ToEventId">Target step.</param>
/// <param name="CrossesBoundary">True when the edge crosses the central ↔ site boundary (drawn in the accent colour).</param>
/// <param name="External">True for an inferred relationship the system did not observe directly — a device echo satisfying a wait after a write (drawn dashed).</param>
public sealed record FlowEdge(Guid FromEventId, Guid ToEventId, bool CrossesBoundary, bool External);
/// <param name="Async">True when the target is a fire-and-forget branch the source did not wait for — a cached call or a notification (drawn dashed, plan D4).</param>
/// <param name="Spawn">True when the target is the first step of a child execution spawned by something other than a route call — a trigger cascade (bolt marker, plan D9).</param>
public sealed record FlowEdge(Guid FromEventId, Guid ToEventId, bool CrossesBoundary, bool External, bool Async = false, bool Spawn = false);
/// <summary>The projected flow of one execution tree (Flow Debugger design 2026-09-05 §7.2).</summary>
/// <param name="Steps">Steps in display order (time, then kind rank, then event id).</param>
@@ -129,7 +129,7 @@ public static class FlowGraphBuilder
FlowStepKind.ScriptRun => 2,
FlowStepKind.AttributeWrite => 3,
FlowStepKind.AttributeWait => 4,
FlowStepKind.External => 5,
FlowStepKind.ApiCall or FlowStepKind.DbCall or FlowStepKind.Notify => 5,
FlowStepKind.Unknown => 6,
FlowStepKind.Response => 9,
_ => 7,
@@ -9,6 +9,8 @@ public enum FlowLane
Site,
/// <summary>Attribute / tag layer: attribute writes and their device echoes.</summary>
Tags,
/// <summary>Actors outside ScadaBridge: outbound API calls, database calls and notifications (v1.1, 2026-09-06 plan D2).</summary>
External,
}
/// <summary>What a flow step represents — the projection of an audit channel/kind pair.</summary>
@@ -26,8 +28,12 @@ public enum FlowStepKind
AttributeWait,
/// <summary>Synthesized final node — the inbound response (from the inbound row's response summary).</summary>
Response,
/// <summary>An outbound API / DB / notification row emitted by a traced run (shown, not chained across the boundary).</summary>
External,
/// <summary>An <c>ApiOutbound</c> row (<c>ExternalSystem.Call</c> / <c>CachedCall</c>) — one node per operation, lifecycle folded in.</summary>
ApiCall,
/// <summary>A <c>DbOutbound</c> row (<c>Database.Query</c> / <c>Execute</c> / <c>CachedWrite</c>).</summary>
DbCall,
/// <summary>A <c>Notification</c> row (<c>Notify.Send</c> and its central delivery).</summary>
Notify,
/// <summary>A channel/kind this build does not recognise — rendered, never dropped.</summary>
Unknown,
}
@@ -45,8 +51,29 @@ public enum FlowStepOutcome
Predicted,
/// <summary>No trace event exists for this position (e.g. site on an older build).</summary>
NotTraced,
/// <summary>An outbound operation whose latest lifecycle row is non-terminal (Submitted / Forwarded / Attempted).</summary>
Pending,
/// <summary>An outbound operation that ended Parked or Discarded — retry/discard lives on the Site Calls / Notification pages.</summary>
Parked,
}
/// <summary>One audit row of a folded outbound operation's lifecycle (<see cref="FlowStep.Lifecycle"/>).</summary>
/// <param name="EventId">The audit row's id.</param>
/// <param name="Kind">Audit kind name (<c>CachedSubmit</c>, <c>ApiCallCached</c>, <c>CachedResolve</c>, <c>NotifyDeliver</c>, …).</param>
/// <param name="Status">Audit status name (<c>Submitted</c>, <c>Forwarded</c>, <c>Attempted</c>, <c>Delivered</c>, <c>Parked</c>, …).</param>
/// <param name="OccurredAtUtc">When the row was written.</param>
/// <param name="Node">Node that wrote it.</param>
/// <param name="ErrorMessage">Error summary on a failed attempt.</param>
/// <param name="DurationMs">The row's own duration, when it has one.</param>
public sealed record FlowLifecycleRow(
Guid EventId,
string Kind,
string Status,
DateTimeOffset OccurredAtUtc,
string? Node,
string? ErrorMessage,
int? DurationMs);
/// <summary>
/// One node of the flow graph — a central-side projection of a <c>ScriptTrace</c> /
/// <c>ApiInbound.InboundRequest</c> <see cref="ZB.MOM.WW.Audit.AuditEvent"/>. Never serialized
@@ -70,4 +97,5 @@ public sealed record FlowStep(
string? Node,
string? Instance,
string? Target,
ScriptTraceExtra? Extra);
ScriptTraceExtra? Extra,
IReadOnlyList<FlowLifecycleRow>? Lifecycle = null);
@@ -63,10 +63,12 @@ public static class FlowStepProjector
&& string.Equals(kind, nameof(AuditKind.InboundRequest), StringComparison.Ordinal))
return FlowStepKind.InboundRequest;
if (string.Equals(channel, nameof(AuditChannel.ApiOutbound), StringComparison.Ordinal)
|| string.Equals(channel, nameof(AuditChannel.DbOutbound), StringComparison.Ordinal)
|| string.Equals(channel, nameof(AuditChannel.Notification), StringComparison.Ordinal))
return FlowStepKind.External;
if (string.Equals(channel, nameof(AuditChannel.ApiOutbound), StringComparison.Ordinal))
return FlowStepKind.ApiCall;
if (string.Equals(channel, nameof(AuditChannel.DbOutbound), StringComparison.Ordinal))
return FlowStepKind.DbCall;
if (string.Equals(channel, nameof(AuditChannel.Notification), StringComparison.Ordinal))
return FlowStepKind.Notify;
return FlowStepKind.Unknown;
}
@@ -385,20 +385,20 @@ public static class FlowShapePredictor
if (receiverText == "ExternalSystem" && member is "Call" or "CachedCall")
{
var (system, dependent) = LiteralOrExpr(args.FirstOrDefault()?.Expression);
_b.AddNode(FlowStepKind.External, _lane, $"ExternalSystem.{member}({system})", dependent, null);
_b.AddNode(FlowStepKind.ApiCall, _lane, $"ExternalSystem.{member}({system})", dependent, null);
return true;
}
if (receiverText == "Database")
{
var (conn, dependent) = LiteralOrExpr(args.FirstOrDefault()?.Expression);
_b.AddNode(FlowStepKind.External, _lane, $"Database.{member}({conn})", dependent, null);
_b.AddNode(FlowStepKind.DbCall, _lane, $"Database.{member}({conn})", dependent, null);
return true;
}
if (receiverText == "Notify" && member == "Send")
{
_b.AddNode(FlowStepKind.External, _lane, "Notify.Send", false, null);
_b.AddNode(FlowStepKind.Notify, _lane, "Notify.Send", false, null);
return true;
}