feat(flow-debugger): outbound calls on the External lane — CLI projection, specs, rig demo + verification step I
CLI flow-debug table now projects through FlowGraphBuilder (one row per step, outbound lifecycles folded, new 'lifecycle' column; per-event fallback kept). Specs: Component-CentralUI (four lanes, node anatomy, pending refresh), Component-AuditLog (central method-script Database.* now audited; replay reads the three outbound channels), Component-InboundAPI (DbOutbound row per helper call), Component-ScriptAnalysis (REQ-SA-7 kinds + Async edges), design doc v1.1 status line, CLAUDE.md bullet, deferred-work register row 111 (Retry/ Discard + ?correlationId= deep links deferred). Rig: seed-flow-outbound-demo.sh (RigRest/RigDb/FlowDemo + OutboundDemo script + FlowOutboundDemo method/key) and verify-delmia-notifier.sh step I (live rows inline, replay until terminal). Rig verification result follows in its own commit. Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using ZB.MOM.WW.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
|
||||
|
||||
@@ -14,11 +16,15 @@ namespace ZB.MOM.WW.ScadaBridge.CLI.Commands;
|
||||
/// error, errorCode, events: [ … ], truncated, … }</c> whose <c>events</c> are canonical
|
||||
/// audit events (each carrying its domain fields inside a <c>detailsJson</c> string). Handed
|
||||
/// to the generic renderer unprojected, the object takes the Property/Value branch and the
|
||||
/// whole event array lands in one cell as raw JSON. This unwraps it to one row per step —
|
||||
/// <c>occurred | kind | lane | target | durationMs | status | error</c> — in the same
|
||||
/// chronological order the Flow Debugger page draws them, so a terminal user can read the
|
||||
/// swimlane top-to-bottom. A trailing <c>note</c> row reports a failed / incomplete run
|
||||
/// (with the error code verbatim, e.g. <c>STANDBY_NODE</c>) or a truncated event list.
|
||||
/// whole event array lands in one cell as raw JSON. This projects the events through the same
|
||||
/// <see cref="FlowGraphBuilder"/> the Flow Debugger page uses and prints one row per STEP —
|
||||
/// <c>occurred | kind | lane | target | durationMs | status | lifecycle | error</c> — in the
|
||||
/// order the page draws them, so a terminal user can read the swimlane top-to-bottom. An
|
||||
/// outbound operation's rows (a cached call's Submit → Forwarded → Attempted → Resolve, a
|
||||
/// notification's Send → Deliver) fold into ONE row whose <c>lifecycle</c> column counts them
|
||||
/// and whose <c>status</c> is the latest row's (outbound-calls plan 2026-09-06, D3). A trailing
|
||||
/// <c>note</c> row reports a failed / incomplete run (with the error code verbatim, e.g.
|
||||
/// <c>STANDBY_NODE</c>) or a truncated event list.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>JSON output is never touched — <c>--format json</c> gets the full raw result.</para>
|
||||
@@ -54,14 +60,21 @@ internal static class FlowDebugTableProjection
|
||||
return json;
|
||||
}
|
||||
|
||||
var rows = new JsonArray();
|
||||
foreach (var evt in events.EnumerateArray())
|
||||
rows.Add(ProjectEvent(evt));
|
||||
var truncated = TryGetPropertyCI(root, "truncated", out var t) && t.ValueKind == JsonValueKind.True;
|
||||
|
||||
var rows = ProjectSteps(events, truncated);
|
||||
if (rows == null)
|
||||
{
|
||||
// The event array did not deserialize as canonical audit events (a future wire
|
||||
// shape, or a proxy body) — fall back to one row per raw event so nothing is hidden.
|
||||
rows = new JsonArray();
|
||||
foreach (var evt in events.EnumerateArray())
|
||||
rows.Add(ProjectEvent(evt));
|
||||
}
|
||||
|
||||
var completed = TryGetPropertyCI(root, "completed", out var c) && c.ValueKind == JsonValueKind.True;
|
||||
var errorCode = StrValue(root, "errorCode");
|
||||
var error = StrValue(root, "error");
|
||||
var truncated = TryGetPropertyCI(root, "truncated", out var t) && t.ValueKind == JsonValueKind.True;
|
||||
|
||||
var notes = new List<string>();
|
||||
if (!completed || errorCode != null || error != null)
|
||||
@@ -81,7 +94,66 @@ internal static class FlowDebugTableProjection
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Projects one canonical audit event (camelCase JSON) to its compact row.</summary>
|
||||
private static readonly JsonSerializerOptions EventOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Builds the flow graph from the canonical events and projects one row per step. Returns
|
||||
/// null when the array is not a list of canonical audit events.
|
||||
/// </summary>
|
||||
private static JsonArray? ProjectSteps(JsonElement events, bool truncated)
|
||||
{
|
||||
List<AuditEvent>? list;
|
||||
try
|
||||
{
|
||||
list = JsonSerializer.Deserialize<List<AuditEvent>>(events.GetRawText(), EventOptions);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (list == null || list.Any(e => e == null || e.EventId == Guid.Empty))
|
||||
return null;
|
||||
|
||||
// Status per event: the graph's outcome is a coarser projection; the table keeps the
|
||||
// audit status word (Delivered / Submitted / Parked …) the operator sees elsewhere.
|
||||
var statusById = new Dictionary<Guid, string?>();
|
||||
foreach (var e in list)
|
||||
statusById[e.EventId] = AuditDetailsCodec.Deserialize(e.DetailsJson).Status;
|
||||
|
||||
var graph = FlowGraphBuilder.Build(list, truncated);
|
||||
var rows = new JsonArray();
|
||||
foreach (var step in graph.Steps)
|
||||
{
|
||||
var lifecycle = step.Lifecycle;
|
||||
var status = lifecycle is { Count: > 0 }
|
||||
? lifecycle[^1].Status
|
||||
: statusById.TryGetValue(step.EventId, out var st) ? st : step.Outcome.ToString();
|
||||
|
||||
var target = step.Title;
|
||||
if (step.Lane != FlowLane.External && step.Instance != null && !target.StartsWith(step.Instance, StringComparison.Ordinal))
|
||||
target = $"{step.Instance}.{target}";
|
||||
|
||||
rows.Add(new JsonObject
|
||||
{
|
||||
["occurred"] = step.OccurredAtUtc.ToUniversalTime().ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture),
|
||||
["kind"] = step.Kind.ToString(),
|
||||
["lane"] = step.Lane.ToString(),
|
||||
["target"] = target,
|
||||
["durationMs"] = step.DurationMs,
|
||||
["status"] = status,
|
||||
["lifecycle"] = lifecycle is { Count: > 0 } ? $"{lifecycle.Count} {(lifecycle.Count == 1 ? "row" : "rows")}" : null,
|
||||
["error"] = step.ErrorMessage,
|
||||
});
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/// <summary>Projects one canonical audit event (camelCase JSON) to its compact row (fallback path).</summary>
|
||||
private static JsonNode ProjectEvent(JsonElement evt)
|
||||
{
|
||||
if (evt.ValueKind != JsonValueKind.Object)
|
||||
|
||||
@@ -2289,9 +2289,14 @@ Trace steps are ordinary audit rows: a run leaves them in the central `AuditLog`
|
||||
chain after the fact.
|
||||
|
||||
Table output prints one row per step in chronological order —
|
||||
`occurred | kind | lane | target | durationMs | status | error` — with a trailing `note`
|
||||
row when the run did not complete (the error code verbatim) or the inline event list was
|
||||
truncated. `--format json` returns the raw `FlowDebugRunResult` (`rootExecutionId`,
|
||||
`occurred | kind | lane | target | durationMs | status | lifecycle | error` — the same
|
||||
projection the Flow Debugger page draws (`FlowGraphBuilder`): an outbound `ExternalSystem.*`,
|
||||
`Database.*` or `Notify` call is ONE row on the `External` lane (kinds `ApiCall` / `DbCall` /
|
||||
`Notify`), and a cached call's or notification's later rows (Forwarded → Attempted → Delivered /
|
||||
Parked) fold into it — `lifecycle` counts them and `status` is the latest row's (`Submitted`
|
||||
while the operation is still in flight; re-run `replay` to see it settle). A trailing `note`
|
||||
row reports a run that did not complete (the error code verbatim) or a truncated inline event
|
||||
list. `--format json` returns the raw `FlowDebugRunResult` (`rootExecutionId`,
|
||||
`completed`, `error`, `errorCode`, `events`, `truncated`, `startedAtUtc`, `finishedAtUtc`).
|
||||
|
||||
#### `flow-debug run-method`
|
||||
@@ -2345,7 +2350,8 @@ scadabridge --url <url> flow-debug replay --execution-id <guid>
|
||||
| `--execution-id` | yes | Any execution ID in the chain — the root is resolved server-side |
|
||||
|
||||
Only flows whose site legs were traced (a Live run, or a method with `--trace-all-calls`
|
||||
set) have site steps to replay; an untraced call shows its central `RouteCall` step alone.
|
||||
set) have site steps to replay; an untraced call shows its central `RouteCall` step plus the
|
||||
run's outbound API / DB / notification rows, which are always audited.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user