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:
Joseph Doherty
2026-09-06 02:10:50 -04:00
parent c3b4068de2
commit 3c9f9da818
14 changed files with 330 additions and 26 deletions
+59
View File
@@ -31,6 +31,10 @@ set -euo pipefail
# G reject path (DownloadPath *.bad) -> NO + "Simulated reject"
# H Flow Debugger live run + replay -> YES (flow-debug run-method returns the traced
# site steps inline; flow-debug replay rebuilds the same chain from the central audit store)
# I Flow Debugger OUTBOUND calls (2026-09-06) -> YES (FlowOutboundDemo — seed-flow-outbound-demo.sh —
# returns ApiOutbound + DbOutbound + Notification rows inline, the two async ones still Submitted;
# replay is retried <= 90 s until both have reached a terminal lifecycle row). Skipped with a
# note when the demo method is not seeded.
: "${DELMIA_API_KEY:?export DELMIA_API_KEY=sbk_... first}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"; PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
@@ -131,4 +135,59 @@ done
[ "${H_RRC:-1}" -eq 0 ] || { echo "FAIL: replay never showed the full chain after 40 s: $H_REPLAY"; exit 1; }
echo " replay: $H_REPLAY"
echo "H live+replay -> YES"
# I — Flow Debugger outbound calls (plan 2026-09-06). The demo script makes a sync ExternalSystem.Call,
# a Database.CachedWrite and a Notify.Send. Live: the site returns the rows it emitted inline — the
# sync ApiCall is already Delivered, the cached write and the notification are Submitted (pending).
# Replay: the S&F forwarder (site) and the Notification Outbox (central) write the later lifecycle
# rows minutes apart, so replay is retried until both operations have a terminal row.
echo "== I Flow Debugger outbound calls (live + replay)"
I_METHOD_ID=$("$CLI" api-method list "${H_ARGS[@]}" | python3 -c 'import json,sys; ms=json.load(sys.stdin); print(next((m["id"] for m in ms if m["name"]=="FlowOutboundDemo"), ""))')
if [ -z "$I_METHOD_ID" ]; then
echo "I outbound live+replay -> SKIPPED (FlowOutboundDemo not seeded — run docker/seed-flow-outbound-demo.sh)"
else
i_check() { # $1 = live|replay; stdin = FlowDebugRunResult JSON
python3 -c '
import json, sys
mode = sys.argv[1]
r = json.load(sys.stdin)
rows = [json.loads(e["detailsJson"]) for e in r.get("events", [])]
by = lambda ch: [d for d in rows if d.get("channel") == ch]
api, db, nt = by("ApiOutbound"), by("DbOutbound"), by("Notification")
terminal = {"Delivered", "Failed", "Parked", "Discarded"}
last = lambda ds: (ds[-1].get("status") if ds else None)
problems = []
if r.get("completed") is not True: problems.append("incomplete")
if not any(d.get("kind") == "ApiCall" and d.get("status") == "Delivered" for d in api): problems.append("no Delivered ApiCall")
if not db: problems.append("no DbOutbound rows")
if not nt: problems.append("no Notification rows")
if mode == "live":
if last(db) != "Submitted": problems.append("db not pending inline")
if last(nt) != "Submitted": problems.append("notify not pending inline")
else:
if last(db) not in terminal: problems.append("db still " + str(last(db)))
if last(nt) not in terminal: problems.append("notify still " + str(last(nt)))
if last(db) != "Delivered": problems.append("db ended " + str(last(db)))
ok = not problems
print(("YES" if ok else "NO") + " root=" + str(r.get("rootExecutionId")) + " events=" + str(len(rows))
+ " api=" + str(last(api)) + " db=" + str(last(db)) + " notify=" + str(last(nt))
+ (" problems=" + ";".join(problems) if problems else "") + (" error=" + str(r.get("errorCode")) if r.get("errorCode") else ""))
sys.exit(0 if ok else 1)' "$1"
}
I_PARAMS=$(python3 -c 'import json,sys; print(json.dumps({"MachineCode":sys.argv[1],"WorkOrder":"WO-I"}))' "$MACHINE")
set +e; I_RUN=$("$CLI" flow-debug run-method --method-id "$I_METHOD_ID" --params "$I_PARAMS" "${H_ARGS[@]}"); I_RC=$?; set -e
[ "$I_RC" -eq 0 ] || { echo "FAIL: flow-debug run-method exited $I_RC: $I_RUN"; exit 1; }
I_LIVE=$(echo "$I_RUN" | i_check live) || { echo "FAIL: live run: $I_LIVE"; exit 1; }
echo " live: $I_LIVE"
I_ROOT=$(echo "$I_RUN" | python3 -c 'import json,sys; print(json.load(sys.stdin)["rootExecutionId"])')
I_REPLAY=""
for i in $(seq 1 45); do
set +e; I_REPLAY=$("$CLI" flow-debug replay --execution-id "$I_ROOT" "${H_ARGS[@]}" | i_check replay); I_RRC=$?; set -e
[ "$I_RRC" -eq 0 ] && break
sleep 2
done
[ "${I_RRC:-1}" -eq 0 ] || { echo "FAIL: replay never showed both outbound operations terminal after 90 s: $I_REPLAY"; exit 1; }
echo " replay: $I_REPLAY"
echo "I outbound live+replay -> YES"
fi
echo "== done. Notifier log: $RUN_DIR/delmia-notifier.log"