Files
ScadaBridge/docs/plans/2026-09-05-flow-debugger-design.md

30 KiB
Raw Permalink Blame History

Flow Debugger — design

Status: implemented 2026-09-06 (branch feat/flow-debugger); plan 2026-09-05-flow-debugger.md (24 tasks, all complete). Residual follow-ups: deferred-work register row 110. v1.1 outbound calls (a fourth External lane for API / DB / notification nodes, lifecycle folding, async/spawn edges, pending refresh) — see the addendum plan 2026-09-06-flow-debugger-outbound-calls.md; §4's "three lanes fixed" is amended by its D2. v1.2 follow-ups (node Retry/Discard, ?correlationId= deep links to the report pages, the prediction circuit-deadlock fix, Playwright coverage) — plan 2026-09-06-flow-debugger-followups.md; register row 111 closed. v1.3 script entry (site → instance → script as a second entry point) — design 2026-09-08-flow-debugger-script-entry-design.md. v1.4 step logs and exceptions (a Log global on every script surface, ScriptTrace.ScriptLog rows folded onto the step that produced them, the failing step's full exception on the node, per-run caps) — design 2026-09-09-flow-debugger-step-logs-design.md, plan 2026-09-09-flow-debugger-step-logs.md; register row 110's console-capture half closed by it. Mockup: Claude Design canvas Flow Debugger Mockup — https://claude.ai/code/artifact/3f3e81e5-8e5e-4333-bdf6-59256108494d (Direction A swimlane boards + two low-fi alternates + decision notes). Owner components: Central UI (#page), Inbound API, Site Runtime, Audit Log, Communication, Script Analysis, Configuration Database.

1. What it is

A Central UI page, Deployment → Flow Debugger, that runs one entry point with operator-supplied inputs and renders every step the run passes through as a swimlane flowchart: lanes are where code ran (Central, the Site the route landed on, the tag source), nodes are steps (request, script run, route call, attribute write, wait, response) each carrying its input, output, duration and status. Clicking a node opens a tabbed detail panel (Input · Output · Attribute writes · Console · Timing).

Worked example, and the acceptance flow for the whole feature: the Delmia recipe download (docs/api-specs/Delmia-Recipe-Download-API.md §4) — inbound POST /api/DelmiaRecipeDownload → central method script → Route.To(MachineCode).Call("ProcessRecipeDownload") → site Call script on the DelmiaReceiver child → seven attribute writes → wait for Galaxy to raise RecipeProcessedFlag → read result → response.

Decisions taken (2026-09-05)

# Decision Rejected
D1 Two modes: Live and Replay. Live is a real run with real values, real writes, audited like any inbound call. Replay executes nothing — it rebuilds the flow from an existing ExecutionId's trace rows. Dry run (execute scripts but capture writes/ES/DB/Notify instead of sending). Needs an interception seam in the site runtime that does not exist and would fork the semantics of every helper.
D2 Two entry points in v1: an Inbound API method, and a value-change-triggered site script on a named instance. Both may land on a site script on instance X. Alarm/timer trigger simulation, direct "call script X" — see §11. Superseded by 2026-09-08-flow-debugger-script-entry-design.md (site → instance → script) as the page's second entry point.
D3 Value-change on a data-sourced attribute does a REAL tag write through the instance actor's normal write path; the device echo fires the trigger. Correlation is by a short-lived trace arm on the instance actor (§5.3). A debug-only "inject value" command that synthesises the change without touching the device — leaves the instance's cached value diverged from the device until the next real update.
D4 Live gets its trace inline; Replay reads audit rows. Site audit telemetry forwards on a 5 s busy / 30 s idle cadence (AuditLog:SiteTelemetry), too slow to drive an interactive run, so the routed reply carries the site steps back and the same steps are also written as audit rows for Replay. Live waiting on the audit pipeline.
D5 Tracing is gated, not always-on. Sites run scripts at high rates from timers and value changes; a trace flag rides beside ParentExecutionId and only flagged runs emit trace rows. One exception is always-on: a central RouteCall row per Route.To().Call (inbound-driven volume, joins the two halves of a Replay). Tracing every run (volume), or a purely time-window join.
D6 Live run is a Deployer act. RequireDeployment gates the Run controls and the two run commands; the page and Replay are open to OperationalAudit roles + Deployer (RequireFlowDebugger, arch-review F3). A separate RequireOperator gate for Live.

2. What exists today (reused, not rebuilt)

  • Correlation ids already thread the chain. AuditWriteMiddleware mints an ExecutionId per inbound request; RouteHelper.Call stamps it as RouteToCallRequest.ParentExecutionId; InstanceActor forwards it on ScriptCallRequest.ParentExecutionId; the site run mints its own id (ScriptRuntimeContext._executionId) and records the parent; a static write carries the writer's id (SetStaticAttributeCommand.SourceExecutionId → AttributeValueChanged.SourceExecutionId) into the value-change / alarm cascade. IAuditLogRepository.GetExecutionTreeAsync walks it.
  • Audit captures the ends. AuditKind.InboundRequest stores the full request and response (up to InboundMaxBytes); ApiCall/DbWrite/NotifySend rows are per-execution.
  • Central already drives live instances. SiteCommandService routing group: RouteToCall, RouteToGetAttributes, RouteToSetAttributes, RouteToWaitForAttribute; the Test Run sandbox's SandboxInstanceGateway runs a central script against a deployed instance over exactly this transport.
  • Roslyn compiles every script centrally (ScriptAnalysisService, ScriptTrustValidator's syntax walk), so the predicted graph is a sibling walker, not new infrastructure.
  • Attribute writes already have one path. WriteAttributeBatchRequest → InstanceActor.HandleWriteAttributeBatch → WriteTagBatchRequest to the DCL for data-sourced attributes; SetStaticAttributeCommand for static ones.

3. What is missing (the build)

Gap Why it blocks Closed by
G1 No record that a site script ran — no AuditKind for a run; per-run site events are opt-in, aggregated, id-less. Replay would show the inbound row and any outbound leaf rows with the site script itself absent. §5.1 ScriptRun trace row.
G2 Attribute writes and waits are invisible — WriteAttributeBatchRequest/WaitForAttributeRequest carry no execution id; nothing records outcomes. The Delmia flow's steps 5–7 cannot be drawn. §5.1 AttributeWrite/AttributeWait rows + §6 additive fields.
G3 Route calls are not audited centrally and the routed reply does not return the child run's id. Central and site halves of a Replay cannot be joined without guessing. §5.1 RouteCall row (always-on) + RouteToCallResponse.ChildExecutionId.
G4 No trace gate. Always-on would flood AuditLog from timer/value-change scripts. §5.2 trace flag + per-method TraceAllCalls.
G5 A value-change entry needs a way to fire the trigger and correlate the echo. Data-sourced echoes arrive as roots. §5.3 trace arm.
G6 No page, no graph builder, no predictor. — §7, §8.
G7 Live runs have no API key, so no actor on the inbound row. Audit would show an anonymous inbound call. §9.

4. User experience

Reference the mockup's two full-fidelity boards; this section only records what the mockup fixes.

Toolbar. Entry point (Inbound API method | Value-change trigger) + a picker for the method or the instance + script; Mode (Live | Replay); Edit inputs, Load from audit…, Run. Superseded by 2026-09-08-flow-debugger-script-entry-design.md: the second entry point is site → instance → script, not site → instance → attribute.

Status strip. State chip (Not run / Running / Completed / Failed / Timed out), run number, wall-clock and node, total duration, counts (steps, boundaries crossed, attribute writes), root ExecutionId linking to the existing Execution Chain page.

Flow panel. Three lanes fixed in v1: Central, Site ({siteId} · {instance}), Tags ({connection} → {source}). Before a run the graph is predicted (§8) — nodes whose target depends on an input value or on live site state render dashed. After a run every node is real; predicted nodes that did not execute (an early-return branch not taken) render greyed. Node = kind eyebrow + duration, title, one-line in/out summary; the selected node gets the accent border. Connectors: solid = executed edge, accent = crossed a gRPC boundary, dashed = external actor (the tag source responding). Zoom −/+/Fit only; no pan-by-drag in v1.

Detail panel (below the flow, tabbed): Input, Output, Attribute writes (attribute · before · written · quality · ack ms), Console, Timing; a right column Where this ran (site/node, instance, composed child, connection, attribute kind, source line) with Open in Debug View and Audit rows (n) drill-ins.

Inputs form (the Step 1 detail before a run): generated from the method's ParameterDefinitions (type, required) or, for a value-change entry, one field for the trigger attribute's new value plus read-only current value/quality. Fill from last real call pre-fills from the most recent InboundRequest row for that method. Superseded by 2026-09-08-flow-debugger-script-entry-design.md: the script entry point's Inputs card is a ParameterValueForm from the script's own ParameterDefinitions for call kinds, and the single value field (attribute derived from the trigger, not picked) for write kinds.

Replay. Load from audit… takes an ExecutionId (or arrives via a new Open in Flow Debugger action on the Audit Log drawer and the Execution Chain page). The graph is built from trace rows (§7.3); a run that was not traced degrades honestly — inbound row + RouteCall row + outbound leaf rows, with the site interior rendered as one collapsed not traced node.

5. Trace model

5.1 Trace rows are audit rows

A new AuditChannel.ScriptTrace with kinds:

Kind Emitted by Payload (DetailsJson)
RouteCall Central RouteHelper.Call (always-on, not gated) target instance, script, args (redacted per existing policy), return value, success/error, duration ms, ChildExecutionId
ScriptRun Site ScriptRunLauncher on run completion (gated) script, instance, trigger {kind, attribute, value, source} (a value-change run's trigger rides here — there is no separate TriggerFired kind), parameters, return value, success/error, duration ms, source node
AttributeWrite Site ScriptRuntimeContext.SetAttribute / WriteBatchAndWait (gated) one row per write or batch: writes[{attribute, written, success, error}], trigger attribute
AttributeWait Site ScriptRuntimeContext.WaitInternal (gated) attribute, predicate/target, timeout, outcome (satisfied/timeout/cancelled/error), elapsed ms, observed value

Who emits AttributeWrite/AttributeWait (planning correction 2026-09-05): every accessor path funnels into three ScriptRuntimeContext methods — SetAttribute, WriteBatchAndWait and WaitInternal — and the context already holds IAuditWriter, _executionId and _parentExecutionId, so the InstanceActor needs no new ids and no new request fields. Batch-write echoes are not armed in v1 — only SetStaticAttributeCommand writes arm (§5.3).

Every row carries the run's ExecutionId and ParentExecutionId exactly as the existing channels do, so the Execution Chain walk, IX_AuditLog_ParentExecution, telemetry forwarding, reconciliation pulls, site 7-day retention and the central purge all apply unchanged. RouteCall is a central-direct-write row (ICentralAuditWriter, SourceNode = central-x); the site kinds go through the site IAuditWriter (SQLite hot path → telemetry → central).

Retention: ScriptTrace gets a per-channel override in AuditLog:PerChannelRetentionDays (rig default 30 days; must stay under the 365-day global window per the existing rule).

5.2 The trace gate

A boolean Trace rides beside ParentExecutionId everywhere the parent id already travels: RouteToCallRequest, RouteToCallResponse (echoed), ScriptCallRequest, SetStaticAttributeCommand, and the new SourceExecutionId carriers in §6. Site-side the flag lands in ScriptRuntimeContext as IsTraced and every helper consults it before emitting a ScriptTrace row. Nested runs inherit it (Scripts.Call → ScriptCallRequest.Trace; static-write cascades → SetStaticAttributeCommand.Trace → AttributeValueChanged.Trace → the spawned run), so a traced root traces its whole tree and nothing else.

Two ways a run becomes traced:

  1. The debugger's Live run sets it on the root.
  2. ApiMethod.TraceAllCalls (new bit NOT NULL DEFAULT 0 column, EF migration, CLI api-method update --trace-all-calls, Central UI checkbox on the method form): every real production call to that method is traced, so Replay of a real Delmia download is complete. Meant for low-volume integration methods; the method form shows the warning that it adds ~5 audit rows per call.

5.3 Value-change entry and the trace arm

The debugger writes the chosen attribute through the instance actor's normal path:

  • Static attribute: RouteToSetAttributes → SetStaticAttributeCommand with SourceExecutionId = <debugger root id>, Trace = true. The existing cascade already stamps the resulting AttributeValueChanged and the spawned run's ParentExecutionId. Nothing new.
  • Data-sourced attribute: the same RouteToSetAttributes → SetStaticAttributeCommand path — InstanceActor already dispatches data-sourced attributes to the DCL (HandleSetDataAttribute → WriteTagRequest); no WriteAttributeBatchRequest change is needed. The device echo comes back through the DCL as an ordinary AttributeValueChanged with no source. Trace arm: when HandleSetDataAttribute sees command.Trace && command.SourceExecutionId != null it records (attribute → sourceExecutionId, expiresAt = now + TraceArmWindow) (default 30 s, SiteRuntimeOptions, one entry per attribute, newest wins). The next DCL value change for that attribute — whatever the value, so a device that clamps or rejects the write still shows what it actually did — is published with SourceExecutionId and Trace set (consumed in HandleTagValueUpdate), and the arm is consumed. An arm that expires unconsumed emits nothing; the debugger renders the trigger node as no echo within 30 s.

The arm exists only for traced writes, so production behaviour is unchanged. Alarm cascades that the echo also fires inherit the parent id exactly as static cascades do today.

5.4 Live-run inline transport

A trace step IS an AuditEvent in channel ScriptTrace — there is no separate wire record. RouteToCallResponse gains additive ChildExecutionId (Guid?) and TraceEvents (IReadOnlyList<AuditEvent>?, the very events that became the site's audit rows, in emission order); on the wire it is repeated AuditEventDto trace_events (the DTO sitestream.proto already defines). The site collects events in a per-run FlowTraceCollector (Commons Types/Debugging/, shared with the central FlowDebugRunner) held by ScriptRuntimeContext and attaches the list to the ScriptCallResult → RouteToCallResponse when Trace is set. The collector applies IAuditRedactor inside Add, so the inline record is the same filtered event the writer persists, and it caps at MaxTraceEventsPerRun (500) or 1 MB of summed DetailsJson (DefaultMaxBytes), whichever comes first; beyond either bound it keeps the earlier events, sets Truncated, and the audit rows remain complete (arch-review F1). A site on an older build ignores Trace, returns no TraceEvents, and the UI labels its lane site not traced (older build). FlowStep is a central-side projection (Commons/Types/Debugging/FlowStepProjector) computed from AuditEvent + AuditDetails; nothing serializes it.

For the value-change entry there is no routed reply to ride on. The runner polls the site's PullTraceEvents RPC by ParentExecutionId (1 s cadence, 2 s settle window, WaitSeconds bound) — simpler than the debug-stream subscription originally planned. AttributeValueUpdate.source_execution_id/trace (fields 7/8) are populated by the site and consumed by nothing; they are reserved for a future debug-stream path and must not be reused. The RPC sits behind the same PSK interceptor as PullAuditEvents and reads the site audit SQLite by ExecutionId / ParentExecutionId — one additive RPC, no streaming change.

6. Message and wire changes (all additive)

Contract Change
RouteToCallRequest + bool Trace = false
RouteToCallResponse + Guid? ChildExecutionId, + IReadOnlyList<AuditEvent>? TraceEvents
ScriptCallRequest / ScriptCallResult + bool Trace; + Guid? ExecutionId, + IReadOnlyList<AuditEvent>? TraceEvents on the result
RouteToSetAttributesRequest + Guid? ParentExecutionId, + bool Trace
SetStaticAttributeCommand, AttributeValueChanged + Guid? SourceExecutionId (where missing), + bool Trace
site_command.proto trace on RouteToCallRequestDto/RouteToSetAttributesRequestDto; child_execution_id + repeated AuditEventDto trace_events on RouteToCallResponseDto (no FlowStepDto); regenerate with docker/regen-proto.sh sitecommand
sitestream.proto + PullTraceEvents RPC (PullTraceEventsRequest { repeated execution_ids, since_utc, max_events } → PullTraceEventsResponse { repeated AuditEventDto events }); AttributeValueUpdate + string source_execution_id = 7, + bool trace = 8 so the debug stream can hand the debugger the spawned id
ApiMethod + TraceAllCalls bit NOT NULL DEFAULT 0 (EF migration + manual SQL script)
AuditChannel/AuditKind ScriptTrace; RouteCall, ScriptRun, AttributeWrite, AttributeWait

FlowStep (Commons Types/Debugging/FlowStep.cs, projection only — never on the wire): EventId, ExecutionId, ParentExecutionId, Kind, Lane (Central/Site/Tags), Title, Subtitle, OccurredAtUtc, DurationMs, Outcome, InputJson, OutputJson, ErrorMessage, SiteId/Node/Instance/Target, and a kind-specific ScriptTraceExtra parsed from AuditDetails.Extra. Field numbers never reused; proto3 defaults are the negotiation, as for stream batching.

7. Central components

7.1 FlowDebugService (Central UI, scoped)

  • PredictAsync(entry) → §8.
  • RunMethodAsync / RunValueChangeAsync / ReplayAsync → Live runs and Replay are FlowDebugRunMethodCommand / FlowDebugRunValueChangeCommand / FlowDebugReplayCommand on the ManagementActor (Deployer role, long-running timeout). The ManagementActor is a per-node actor (AkkaHostedService, /user/management), not a singleton. The run-method handler consults IActiveNodeGate and refuses on a standby with error code STANDBY_NODE naming the design rule (Component-InboundAPI: method scripts and Route.To never run on a standby); Traefik routes UI traffic to the active node so this is rare. The Central UI calls it through ManagementActorHolder (the DataConnectionMoveService pattern) and the CLI through /management — one path, one audit story. The inbound row for a Live run is written by the handler via ICentralAuditWriter (kind InboundRequest, actor = operator username, Extra.source = "flow-debugger"). Inbound entry: InboundScriptExecutor.ExecuteAsync(method, inputs, route, timeout, parentExecutionId: root, trace: true) with a RouteHelper whose Trace flag is set and whose inline trace sink collects the RouteCall row plus the site's TraceEvents. Value-change entry: §5.3 + §5.4.
  • ReplayAsync(executionId) → IAuditLogRepository.GetExecutionTreeAsync + a new GetTraceEventsAsync(rootExecutionId) (all ScriptTrace rows in the tree, plus the InboundRequest row) → FlowGraphBuilder.

7.2 FlowGraphBuilder (pure, Commons or Central UI)

Steps → FlowGraph { Lanes, Nodes, Edges }. Lane assignment by Where (central node → Central; site node → Site; AttributeWrite → Tags). Edges follow emission order within an execution and parent→child across executions; an edge whose endpoints sit on different lanes is marked CrossesBoundary. A predicted graph and a real graph share the model; the page overlays them (predicted-but-not-executed → greyed).

7.3 Page and renderer

Components/Pages/Deployment/FlowDebugger.razor (+ .razor.cs), nav item under Deployment after Debug View. Renderer is a custom SVG component FlowGraphView (no third-party charting or diagram libraries, matching KpiTrendChart): fixed 110 px lanes, nodes placed by topological column, connectors as orthogonal paths with arrow markers; zoom via a viewBox scale. Detail panel is a plain Bootstrap tab strip. Persisted per-browser: last entry point and inputs (same localStorage pattern Debug View uses for its selection).

8. Predicted graph (static analysis)

FlowShapePredictor in Script Analysis (#25), a CSharpSyntaxWalker beside ScriptTrustValidator.HardeningWalker, over the entry script and every script it can reach:

  • Recognised sites: Route.To(x).Call(name, …) / .SetAttributes / .WaitForAttribute / .GetAttributes; Scripts.Call(name) / CallShared; Attributes["a"] = v, Children["c"].Attributes["a"] = v; WriteBatchAndWaitAsync(…), WaitAsync(…), WaitForAsync(…); ExternalSystem.Call/CachedCall; Database.*; Notify.Send; return.
  • Constant string arguments resolve; anything else yields a dashed node (target = input value). Route.To(constant) resolves the instance → template → the named script's body, which is then walked too (one hop; depth cap 4, the same as the runtime call-depth cap).
  • Branches: an if … return before a recognised site becomes an early return annotation on the edge, as the mockup shows; the predictor does not attempt path-sensitivity beyond that.
  • Output is a FlowGraph with Predicted = true; the page renders it before the run and overlays the real graph after.

Compile is the existing cached compile (CachingScriptMetadataResolver attached like every other surface); the walker is syntax-only and never executes anything.

9. Security, authorisation, audit

  • Page visibility: RequireFlowDebugger — the OperationalAudit roles plus Deployer (AuthorizationPolicies.FlowDebuggerAccessRoles = Deployer, Administrator, Viewer), so an auditor can open the page and Replay a past execution (arch-review F3). Live run: RequireDeployment (same as Debug View) — decided 2026-09-05: a Live run writes real tags, but the Deployer already deploys and redeploys the scripts that do exactly that, so no separate Operator gate. The two run commands, the Live controls, the parked-node Retry/Discard actions and the ?methodId=/?instanceId= drill-ins stay Deployer-only, and the page pins Replay mode for a non-Deployer. Replay: FlowDebugReplayCommand accepts the same OperationalAudit roles (it reads audit rows). No Verifier step — the debugger is a diagnostic, not a production write control, and the audit row is the safeguard.
  • Inbound audit row for a Live run: the shared AuditEvent.Actor carries the operator's username (today it carries the API-key name); Extra.source = "flow-debugger", Extra.runNumber; Target is the method as usual. API-key authentication is bypassed (there is no key), parameter validation is not — ParameterValidator runs exactly as for HTTP so a bad input fails the same way. The method's TimeoutSeconds applies. Site-scope (SiteScopeService) restricts which instances a value-change entry may target. The method entry enforces the operator's site scope at route time (RouteHelper.WithSiteScope, SITE_SCOPE_DENIED → ManagementUnauthorized, arch-review F2): the runner resolves the operator's permitted numeric site ids to site identifiers and hands them to RouteHelper, which refuses an out-of-scope target before any site traffic; the denial is rethrown as SiteScopeViolationException after the inbound audit row is written, so the attempt is recorded under the operator's name. Administrators and unscoped users are exempt, exactly as EnforceSiteScope treats them.
  • Trace payloads pass through the existing redaction policy on BOTH copies: the stored row (writer) and the inline Live copy (FlowTraceCollector applies IAuditRedactor and a 1 MB byte budget beside the 500-event cap — arch-review F1). The row-level caps are unchanged (auth headers redacted, 8 KB cap, 64 KB on error rows). Console output is capped the same way.
  • Nothing here touches the PSK boundary; PullTraceEvents sits behind ControlPlaneAuthInterceptor.

10. Volume and failure behaviour

  • Untraced production is unchanged except one RouteCall row per routed call (inbound-driven). Every routed call adds one central RouteCall insert — at inbound-driven rates that doubles the per-call central audit write (accepted, D5).
  • A traced Delmia call emits ≈ 1 InboundRequest + 1 RouteCall + 1 ScriptRun + 2 AttributeWrite + 1 AttributeWait + 1 value-change ScriptRun ≈ 7–10 rows (a cached DB write and a notification add 3 trace-only rows). TraceAllCalls on a 100-call/day method is ~700–1 000 rows/day.
  • Trace emission is best-effort like all audit (a failed trace write never fails the run).
  • Live run timeout = method timeout + FlowDebugRunGrace (10 s); a timeout renders whatever steps arrived and marks the run Timed out. The site's trace rows still land later, so Replay of the same ExecutionId is complete. The client waits longer than the server on purpose: the UI derives its Ask wait from the same bound (RunAskTimeoutFor = method timeout + grace + 15 s slack, floored at 30 s; ValueChangeAskTimeoutFor = the wait window + 15 s) and the CLI's run-method --timeout-seconds defaults to 120 while run-value-change waits --wait-seconds + 25 (arch-review F4).
  • The Live run's inbound row is written AFTER the run completes, so a Replay taken during a long Live run shows only the RouteCall.
  • Older site build: no TraceEvents, lane labelled; older central build: ignores new fields; proto3 defaults negotiate as for stream batching.
  • Mixed-version caveat: an OLD central ingesting ScriptTrace rows from a NEW site parses unknown channel/kind names to the AuditRowProjection fallbacks (ApiInbound/InboundRequest); upgrade central first.

11. Out of scope (recorded so they are not rediscovered)

  • Dry run (D1). Alarm-trigger and timer-trigger entry points. Step-into for CallShared inline shared scripts (they execute inline; only Scripts.Call spawns a traceable child). Live step-by-step / breakpoints. Editing a script from the debugger. Cross-run comparison. A Vertical trace list toggle (mockup Direction B) — cheap follow-up on the same FlowGraph. Line-level pinning (Direction C) — needs per-statement instrumentation. Console output capture on ScriptRun rows (no script console exists today).

12. Testing

  • Pure: FlowGraphBuilderTests (lane/edge/boundary rules, truncation), FlowShapePredictorTests (each recognised site, constant vs input-dependent target, one-hop route resolution, depth cap, early-return annotation), FlowTraceCollectorTests (cap + Truncated).
  • Site runtime: ScriptRuntimeContext traced write emits AttributeWrite; InstanceActor arms; echo consumes the arm and stamps SourceExecutionId/Trace; expiry emits nothing; untraced write emits nothing and arms nothing (the production-unchanged pin). ScriptRunLauncher emits ScriptRun only when traced. ScriptRuntimeContext.WaitInternal emits AttributeWait for satisfied/timeout.
  • Inbound API: RouteHelper.Call always writes RouteCall; ChildExecutionId round-trips; Trace propagates to RouteToCallRequest.
  • Communication: DTO mapper round-trips for every new field; an old-shape response (no TraceEvents) maps to null.
  • Audit: new kinds accepted by ingest; per-channel retention deletes ScriptTrace rows early; GetTraceEventsAsync returns the tree's rows in order.
  • Central UI (bUnit): page renders predicted graph, overlays real graph, Run button hidden without Deployer, Replay degrades to the collapsed not traced node.
  • Rig: extend docker/verify-delmia-notifier.sh with step H — a Live run of DelmiaRecipeDownload against DelmiaSim01 from the page's service, asserting 9 steps, and a Replay of the same ExecutionId producing the same graph.

13. Documents to update with the implementation

Component-CentralUI.md (new page), Component-InboundAPI.md (RouteCall row, TraceAllCalls, Live-run actor), Component-SiteRuntime.md (trace collector, trace arm, gated emission), Component-AuditLog.md (ScriptTrace channel, retention override, PullTraceEvents), Component-Communication.md (proto changes), Component-ScriptAnalysis.md (predictor), Component-ConfigurationDatabase.md (migration), README.md component table if a component is added (none planned — the debugger is a Central UI page plus seams in existing components), CLAUDE.md key-decisions bullet, CLI README (--trace-all-calls), deferred-work register row for the Direction B list toggle.