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:
@@ -57,8 +57,9 @@ boundary:
|
||||
| `POST /api/{method}` (Inbound API) | External | Inbound (invokes a script) | No (gap) |
|
||||
| `Route.To(x).Call(y)` (Inbound API → site) | Script | Central → site | Yes — `ScriptTrace.RouteCall`, **always-on** (2026-09-05) |
|
||||
| Site script run / attribute write / attribute wait under a **traced** run | Script | Site-local | Yes — `ScriptTrace.ScriptRun` / `AttributeWrite` / `AttributeWait`, **gated** by the `Trace` flag (Flow Debugger Live runs, `ApiMethod.TraceAllCalls`) |
|
||||
| Central method-script `Database.QuerySingle/Query/Execute` (`InboundDatabaseHelper`) | Script | Outbound | Yes — `DbOutbound.DbWrite` via `ICentralAuditWriter` (2026-09-06; was an unaudited gap) — `Target` = connection name, `Actor` = `api:{method}`, the request's `ExecutionId`, `Extra` = `{op, rowsAffected\|rowsReturned}` |
|
||||
|
||||
**`AuditChannel.ScriptTrace` (Flow Debugger, design `docs/plans/2026-09-05-flow-debugger-design.md` §5.1).** Trace steps ARE audit events — no parallel store. One kind is always-on (`RouteCall`, a central direct write per `Route.To().Call`, carrying the child run's id in `Extra.childExecutionId`); the three site kinds are emitted only when the run's `Trace` flag is set, ride the ordinary site `IAuditWriter` → SQLite → telemetry → central path, share the run's `ExecutionId` / `ParentExecutionId`, and are additionally returned inline on the routed reply so a Live run does not wait on the 5 s telemetry cadence. The Flow Debugger's graph (`FlowStep` / `FlowGraphBuilder`, Commons) is a central-side projection of these rows.
|
||||
**`AuditChannel.ScriptTrace` (Flow Debugger, design `docs/plans/2026-09-05-flow-debugger-design.md` §5.1).** Trace steps ARE audit events — no parallel store. One kind is always-on (`RouteCall`, a central direct write per `Route.To().Call`, carrying the child run's id in `Extra.childExecutionId`); the three site kinds are emitted only when the run's `Trace` flag is set, ride the ordinary site `IAuditWriter` → SQLite → telemetry → central path, share the run's `ExecutionId` / `ParentExecutionId`, and are additionally returned inline on the routed reply so a Live run does not wait on the 5 s telemetry cadence. The Flow Debugger's graph (`FlowStep` / `FlowGraphBuilder`, Commons) is a central-side projection of these rows — **and, since 2026-09-06, of the run's `ApiOutbound` / `DbOutbound` / `Notification` rows too** (plan `docs/plans/2026-09-06-flow-debugger-outbound-calls.md`): the outbound rows already sit in the execution tree under the run's `ExecutionId`, so the replay query (`GetTraceEventsAsync`) simply widens its channel filter to the four channels plus the inbound row, and a traced site run also passes the outbound rows it emits through the same inline trace collector as its `ScriptTrace` rows (same `AuditEvent` instance the writer receives — no duplicate construction; a collector that throws can neither fail the call nor skip the durable write). Rows written after the run returned (the S&F forwarder's `Forwarded` / `Attempted` / `CachedResolve`, the Notification Outbox's `NotifyDeliver`) are not inline — the page re-reads the tree for them.
|
||||
|
||||
Out of scope — framework traffic is not audited:
|
||||
|
||||
|
||||
@@ -216,8 +216,9 @@ Per-leaf alarm rendering (leaf nodes are individual conditions for native alarms
|
||||
- **Computed alarms render unchanged** from the prior flat-table style; the enrichment is purely additive for native rows.
|
||||
|
||||
### Flow Debugger (Deployment Role)
|
||||
- **`/deployment/flow-debugger`** (nav: Deployment → Flow Debugger; design `docs/plans/2026-09-05-flow-debugger-design.md`, plan `docs/plans/2026-09-05-flow-debugger.md`). Runs one entry point with operator-supplied inputs and renders every step the run passes through as a **three-lane swimlane** (Central / Site / Tags) — request, route call, site script run, attribute writes, attribute waits, response — each node carrying its input, output, duration and outcome; a tabbed detail panel (Input · Output · Writes · Timing, plus a *Where this ran* column) opens on the selected node with `Open in Debug View` (value-change entries, where the instance is known) and `Audit rows (n)` (→ `/audit/log?executionId=`) drill-ins.
|
||||
- **Two modes.** *Live* is a real run — the method script executes on the active central node with tracing on, or a real attribute write goes through the routed-write path and the site's triggered run is polled back — audited like any other call. *Replay* executes nothing: it rebuilds the flow from an existing execution's `ScriptTrace` audit rows (`?executionId=` drill-in from the Audit Log drawer's and the Execution Chain page's **Open in Flow Debugger** actions).
|
||||
- **`/deployment/flow-debugger`** (nav: Deployment → Flow Debugger; design `docs/plans/2026-09-05-flow-debugger-design.md`, plan `docs/plans/2026-09-05-flow-debugger.md`). Runs one entry point with operator-supplied inputs and renders every step the run passes through as a **four-lane swimlane** (Central / Site / Tags / **External**) — request, route call, site script run, attribute writes, attribute waits, **outbound API / database / notification calls**, response — each node carrying its input, output, duration and outcome; a tabbed detail panel (Input · Output · Writes *or* Lifecycle · Timing, plus a *Where this ran* column) opens on the selected node with `Open in Debug View` (value-change entries, where the instance is known) and `Audit rows (n)` (→ `/audit/log?executionId=`) drill-ins.
|
||||
- **Two modes.** *Live* is a real run — the method script executes on the active central node with tracing on, or a real attribute write goes through the routed-write path and the site's triggered run is polled back — audited like any other call. *Replay* executes nothing: it rebuilds the flow from an existing execution's `ScriptTrace` audit rows plus its `ApiOutbound` / `DbOutbound` / `Notification` rows (`?executionId=` drill-in from the Audit Log drawer's and the Execution Chain page's **Open in Flow Debugger** actions).
|
||||
- **External lane (v1.1, plan `docs/plans/2026-09-06-flow-debugger-outbound-calls.md`).** Every `ExternalSystem.Call/CachedCall`, `Database.*` and `Notify.Send` a traced run makes is a node on the fourth lane — kinds `ApiCall` / `DbCall` / `Notify` (eyebrows `API CALL` / `DATABASE` / `NOTIFY`), title = the system.method / connection / list, a status pill (`Ok` / `Pending` / `Parked` / `Failed`) plus `sync` | `cached` | `notify`. **One node per operation:** a cached call's or notification's later lifecycle rows (Forwarded → Attempted → Delivered / Parked, written minutes later by the S&F forwarder or the Notification Outbox) fold into the node (`FlowStep.Lifecycle`, a Lifecycle tab in the detail panel with *Delivered by*) instead of drawing four boxes. A sync call sits in the execution chain; a cached call or notification hangs off it on a dashed **async** connector, and a trigger cascade's spawn edge carries a bolt marker. A Live run returns only the rows the run itself emitted, so a graph left with `Pending` nodes **refreshes itself** through the Replay command every 5 s for up to 2 min (page constants), and a *Refresh outcomes* button re-reads on demand in either mode; the state chip stays *Completed* — pending outbound work is not a failed run. Retry/Discard stay on the Site Calls / Notification pages (register row 111).
|
||||
- **Two entry points.** An inbound API method (inputs form generated from the method's `ParameterDefinitions`, values coerced by declared type) or a value-change trigger on a named instance attribute (Site → Instance → Attribute pickers, site-scope filtered).
|
||||
- **Predicted graph first.** Choosing an entry point renders the statically predicted shape (`FlowShapePredictor`, Script Analysis REQ-SA-7) before anything runs — nodes whose target depends on inputs render dashed; after a run, predicted nodes the run never produced render greyed.
|
||||
- **Execution path.** `IFlowDebugService` dispatches `FlowDebugRunMethodCommand` / `FlowDebugRunValueChangeCommand` / `FlowDebugReplayCommand` to the **per-node `ManagementActor`** through `ManagementActorHolder` (the same seam the HTTP `/management` endpoint uses), so the server enforces the Deployer gate and the active-node refusal. A standby node answers the two run commands with `STANDBY_NODE`, which the page renders as an inline alert with the server message verbatim; Replay is served from any node. The same three commands back the CLI's `flow-debug run-method | run-value-change | replay`.
|
||||
|
||||
@@ -277,6 +277,8 @@ The helper API (all asynchronous — scripts `await` them; bounded by the method
|
||||
- `await Database.QueryAsync("connectionName", sql, parameters)` — all rows as case-insensitive column→value dictionaries.
|
||||
- `await Database.ExecuteAsync("connectionName", sql, parameters)` — run a write (INSERT/UPDATE/DELETE/DDL); returns rows affected. **Writes are permitted** — the move-in integration records results, not just reads them.
|
||||
|
||||
**Every helper call is audited (2026-09-06).** Each `QuerySingle` / `Query` / `Execute` writes one `AuditChannel.DbOutbound` / `AuditKind.DbWrite` row through `ICentralAuditWriter` — `Status` `Delivered` or `Failed`, `Target` = the connection name, `Actor` = `api:{methodName}`, the request's `ExecutionId`, `DurationMs`, the SQL + bound parameter names/values as `RequestSummary` (capped and redacted at write time by the shared audit redactor exactly like a site `AuditingDbCommand` row), `Extra` = `{"op":"read","rowsReturned":n}` / `{"op":"write","rowsAffected":n}`, `SourceNode` from `INodeIdentityProvider`. Best-effort: an audit fault never reaches the script, and the original exception still rethrows. Under a Flow Debugger Live run the same instance is also handed to the route's trace sink so the call appears as a `DbCall` node on the External lane (Component-CentralUI.md → Flow Debugger). This closed the outbound-DB gap Component-AuditLog.md's scope table used to list for central method scripts.
|
||||
|
||||
Containment rules (enforced, not advisory):
|
||||
|
||||
- **Named connections only.** `connectionName` selects one of the connections configured
|
||||
|
||||
@@ -222,13 +222,14 @@ The decorator changes **no resolution result**, only object identity: it returns
|
||||
|
||||
### REQ-SA-7: Flow Shape Predictor (`FlowShapePredictor`)
|
||||
|
||||
`FlowShapePredictor` (Flow Debugger design `docs/plans/2026-09-05-flow-debugger-design.md` §8) predicts, from script **syntax** alone, the swimlane shape a run would produce before anything executes: `PredictInboundMethod(methodName, methodScript, resolveRouteScript, maxDepth = 4)` and `PredictSiteScript(scriptName, scriptBody, maxDepth = 4)` return a `PredictedFlow` of `PredictedNode(Id, Kind, Lane, Title, InputDependent, Note)` + `PredictedEdge(From, To, CrossesBoundary)`.
|
||||
`FlowShapePredictor` (Flow Debugger design `docs/plans/2026-09-05-flow-debugger-design.md` §8) predicts, from script **syntax** alone, the swimlane shape a run would produce before anything executes: `PredictInboundMethod(methodName, methodScript, resolveRouteScript, maxDepth = 4)` and `PredictSiteScript(scriptName, scriptBody, maxDepth = 4)` return a `PredictedFlow` of `PredictedNode(Id, Kind, Lane, Title, InputDependent, Note)` + `PredictedEdge(From, To, CrossesBoundary, Async = false)`.
|
||||
|
||||
- **Syntax walk, not semantic.** A `CSharpSyntaxWalker` over `CSharpSyntaxTree.ParseText(code, SourceCodeKind.Script)` matches the script-API member-access chains by whitespace-stripped text, exactly as `ScriptTrustValidator`'s hardening walker does: `Route.To(x).Call(y)` / `.SetAttributes` / `.WaitForAttribute`, `Scripts.Call(y)`, `…Attributes.WriteBatchAndWaitAsync` / `WaitAsync` / `WaitForAsync` / `SetAsync`, `…Attributes[x] = v`, `ExternalSystem.Call/CachedCall`, `Database.*`, `Notify.Send`. Reads (`GetAttributes`, indexer reads) produce no node because the runtime records no trace row for them.
|
||||
- **Syntax walk, not semantic.** A `CSharpSyntaxWalker` over `CSharpSyntaxTree.ParseText(code, SourceCodeKind.Script)` matches the script-API member-access chains by whitespace-stripped text, exactly as `ScriptTrustValidator`'s hardening walker does: `Route.To(x).Call(y)` / `.SetAttributes` / `.WaitForAttribute`, `Scripts.Call(y)`, `…Attributes.WriteBatchAndWaitAsync` / `WaitAsync` / `WaitForAsync` / `SetAsync`, `…Attributes[x] = v`, `ExternalSystem.Call/CachedCall`, `Database.Execute/Query/QuerySingle/CachedWrite`, `Notify.Send(list)` / `Notify.To(list).Send(...)` (`Async`-suffixed spellings map identically). Reads (`GetAttributes`, indexer reads) produce no node because the runtime records no trace row for them; nor does `Database.Connection(...)` — the number of commands on a raw handle is not statically knowable.
|
||||
- **Outbound calls land on the External lane (2026-09-06, plan `docs/plans/2026-09-06-flow-debugger-outbound-calls.md` D8).** `ExternalSystem.*` → `ApiCall` titled `{system}.{method}`; `Database.*` → `DbCall` titled `{connection} · {member}`; `Notify` → `Notify` titled `Notify {list}`. A `CachedCall`, `CachedWrite` or `Notify` node carries `Note = "cached"` where applicable and its incoming edge is `Async`; an async edge does **not** advance the chain — the next node links from the previous non-async node — mirroring `FlowGraphBuilder.IsAsync` so a predicted async branch renders like a real one.
|
||||
- **Literals resolve; everything else is `InputDependent`** and titled with the expression's source text (`Route.To(code)`). Branching is not modelled: every recognised call is emitted once in source order, and an `if (…) return/throw` guard becomes a `Note = "early return: <condition>"` on the node that **follows** it (the condition is visited first, so a guard whose condition *is* the traced call — Delmia's `if (!await …WriteBatchAndWaitAsync(…)) return …` — attributes correctly).
|
||||
- **DB-free by contract.** Routed and nested script bodies come from the caller-supplied `resolveRouteScript(instanceCode, scriptName)` (Central UI resolves instance → template parent chain → script; called with the literal code when constant, else with the expression text so a resolver may still answer); recursion stops at `maxDepth` with a `depth limit` note.
|
||||
- **Total.** Parse errors or any walker fault degrade to the request/response pair (or the root `ScriptRun`) carrying `Note = "parse error"`; the predictor never throws into the page.
|
||||
- Predicted kinds are exactly the traced kinds (`FlowStepKind`), so `FlowGraphView` can overlay the real graph on the prediction by kind and grey the nodes a run never produced. There is deliberately **no separate central "method script" node** — the real graph records none, so the `InboundRequest` node stands for it.
|
||||
- Predicted kinds are exactly the traced kinds (`FlowStepKind`, including `ApiCall` / `DbCall` / `Notify`), so `FlowGraphView` can overlay the real graph on the prediction by kind and grey the nodes a run never produced. There is deliberately **no separate central "method script" node** — the real graph records none, so the `InboundRequest` node stands for it.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user