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:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.Commons.Tests.Debugging;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<FlowStepKind>().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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user