From cc1bbf3975f6e17ad888dc59375c8da5b94d3788 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 6 Sep 2026 01:37:29 -0400 Subject: [PATCH] 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 --- .../Components/Shared/FlowGraphView.razor.cs | 4 +- .../Types/Debugging/FlowGraph.cs | 4 +- .../Types/Debugging/FlowGraphBuilder.cs | 2 +- .../Types/Debugging/FlowStep.cs | 34 +++++++++++-- .../Types/Debugging/FlowStepProjector.cs | 10 ++-- .../FlowShapePredictor.cs | 6 +-- .../Debugging/FlowStepProjectorTests.cs | 50 +++++++++++++++++++ .../FlowShapePredictorTests.cs | 4 +- 8 files changed, 99 insertions(+), 15 deletions(-) create mode 100644 tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Debugging/FlowStepProjectorTests.cs diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/FlowGraphView.razor.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/FlowGraphView.razor.cs index fdd5f487..ebf1e14f 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/FlowGraphView.razor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/FlowGraphView.razor.cs @@ -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", }; diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraph.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraph.cs index b4a7bbf7..e8afef16 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraph.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraph.cs @@ -5,7 +5,9 @@ namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging; /// Target step. /// True when the edge crosses the central ↔ site boundary (drawn in the accent colour). /// True for an inferred relationship the system did not observe directly — a device echo satisfying a wait after a write (drawn dashed). -public sealed record FlowEdge(Guid FromEventId, Guid ToEventId, bool CrossesBoundary, bool External); +/// 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). +/// 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). +public sealed record FlowEdge(Guid FromEventId, Guid ToEventId, bool CrossesBoundary, bool External, bool Async = false, bool Spawn = false); /// The projected flow of one execution tree (Flow Debugger design 2026-09-05 §7.2). /// Steps in display order (time, then kind rank, then event id). diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraphBuilder.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraphBuilder.cs index 152e1c7f..3e98d657 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraphBuilder.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowGraphBuilder.cs @@ -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, diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStep.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStep.cs index 53be7d1e..e69e2234 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStep.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStep.cs @@ -9,6 +9,8 @@ public enum FlowLane Site, /// Attribute / tag layer: attribute writes and their device echoes. Tags, + /// Actors outside ScadaBridge: outbound API calls, database calls and notifications (v1.1, 2026-09-06 plan D2). + External, } /// What a flow step represents — the projection of an audit channel/kind pair. @@ -26,8 +28,12 @@ public enum FlowStepKind AttributeWait, /// Synthesized final node — the inbound response (from the inbound row's response summary). Response, - /// An outbound API / DB / notification row emitted by a traced run (shown, not chained across the boundary). - External, + /// An ApiOutbound row (ExternalSystem.Call / CachedCall) — one node per operation, lifecycle folded in. + ApiCall, + /// A DbOutbound row (Database.Query / Execute / CachedWrite). + DbCall, + /// A Notification row (Notify.Send and its central delivery). + Notify, /// A channel/kind this build does not recognise — rendered, never dropped. Unknown, } @@ -45,8 +51,29 @@ public enum FlowStepOutcome Predicted, /// No trace event exists for this position (e.g. site on an older build). NotTraced, + /// An outbound operation whose latest lifecycle row is non-terminal (Submitted / Forwarded / Attempted). + Pending, + /// An outbound operation that ended Parked or Discarded — retry/discard lives on the Site Calls / Notification pages. + Parked, } +/// One audit row of a folded outbound operation's lifecycle (). +/// The audit row's id. +/// Audit kind name (CachedSubmit, ApiCallCached, CachedResolve, NotifyDeliver, …). +/// Audit status name (Submitted, Forwarded, Attempted, Delivered, Parked, …). +/// When the row was written. +/// Node that wrote it. +/// Error summary on a failed attempt. +/// The row's own duration, when it has one. +public sealed record FlowLifecycleRow( + Guid EventId, + string Kind, + string Status, + DateTimeOffset OccurredAtUtc, + string? Node, + string? ErrorMessage, + int? DurationMs); + /// /// One node of the flow graph — a central-side projection of a ScriptTrace / /// ApiInbound.InboundRequest . Never serialized @@ -70,4 +97,5 @@ public sealed record FlowStep( string? Node, string? Instance, string? Target, - ScriptTraceExtra? Extra); + ScriptTraceExtra? Extra, + IReadOnlyList? Lifecycle = null); diff --git a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStepProjector.cs b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStepProjector.cs index 723e0d4a..0803c7ad 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStepProjector.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Commons/Types/Debugging/FlowStepProjector.cs @@ -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; } diff --git a/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/FlowShapePredictor.cs b/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/FlowShapePredictor.cs index 7bd313a5..2e6468b6 100644 --- a/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/FlowShapePredictor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.ScriptAnalysis/FlowShapePredictor.cs @@ -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; } diff --git a/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Debugging/FlowStepProjectorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Debugging/FlowStepProjectorTests.cs new file mode 100644 index 00000000..fe9e8731 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.Commons.Tests/Debugging/FlowStepProjectorTests.cs @@ -0,0 +1,50 @@ +using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging; + +namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Debugging; + +/// +/// Flow Debugger outbound-calls plan (2026-09-06) Task 0: the Commons vocabulary — External lane, +/// the three outbound kinds, Pending/Parked outcomes, the lifecycle row, and additive edge/step shapes. +/// +public class FlowStepProjectorTests +{ + private static readonly DateTimeOffset T0 = new(2026, 9, 6, 12, 0, 0, TimeSpan.Zero); + + [Fact] + public void Vocabulary_HasExternalLane_ThreeOutboundKinds_AndPendingParkedOutcomes() + { + Assert.Equal(3, (int)FlowLane.External); + Assert.True(Enum.IsDefined(FlowStepKind.ApiCall)); + Assert.True(Enum.IsDefined(FlowStepKind.DbCall)); + Assert.True(Enum.IsDefined(FlowStepKind.Notify)); + Assert.True(Enum.IsDefined(FlowStepKind.Unknown)); + Assert.False(Enum.GetNames().Contains("External")); + Assert.True(Enum.IsDefined(FlowStepOutcome.Pending)); + Assert.True(Enum.IsDefined(FlowStepOutcome.Parked)); + } + + [Fact] + public void FlowStep_WithoutLifecycle_StillConstructs_AndDefaultsToNull() + { + var step = new FlowStep(Guid.NewGuid(), null, null, FlowStepKind.ScriptRun, FlowLane.Site, "S", null, + T0, null, FlowStepOutcome.Ok, null, null, null, null, null, null, null, null); + Assert.Null(step.Lifecycle); + + var row = new FlowLifecycleRow(Guid.NewGuid(), "CachedSubmit", "Submitted", T0, "node-a", null, null); + var withLifecycle = step with { Lifecycle = [row] }; + Assert.Single(withLifecycle.Lifecycle!); + Assert.Equal("Submitted", withLifecycle.Lifecycle![0].Status); + } + + [Fact] + public void FlowEdge_AsyncAndSpawn_DefaultFalse() + { + var a = Guid.NewGuid(); + var b = Guid.NewGuid(); + var edge = new FlowEdge(a, b, false, false); + Assert.False(edge.Async); + Assert.False(edge.Spawn); + Assert.True((edge with { Async = true }).Async); + Assert.True(new FlowEdge(a, b, false, false, Async: false, Spawn: true).Spawn); + } +} diff --git a/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/FlowShapePredictorTests.cs b/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/FlowShapePredictorTests.cs index 084b6ef4..1fa20161 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/FlowShapePredictorTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.ScriptAnalysis.Tests/FlowShapePredictorTests.cs @@ -199,8 +199,8 @@ public class FlowShapePredictorTests (FlowStepKind.ScriptRun, "Handshake"), (FlowStepKind.AttributeWrite, "Cmd"), (FlowStepKind.AttributeWait, "Ack"), - (FlowStepKind.External, "ExternalSystem.Call(Mes)"), - (FlowStepKind.External, "Notify.Send"), + (FlowStepKind.ApiCall, "ExternalSystem.Call(Mes)"), + (FlowStepKind.Notify, "Notify.Send"), ], flow.Nodes.Select(n => (n.Kind, n.Title)).ToList()); Assert.DoesNotContain(flow.Nodes, n => n.Lane == FlowLane.Central); Assert.All(flow.Edges, e => Assert.False(e.CrossesBoundary));