Commit Graph

1609 Commits

Author SHA1 Message Date
Joseph Doherty 688136de85 fix(central-ui): Flow Debugger prediction overlay matches by kind and title (arch-review F10)
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 14:10:44 -04:00
Joseph Doherty 0ccbe140ae perf(audit-log): cached-call forwarder enqueues the audit row instead of awaiting its commit (194 ms -> 25 ms)
Measuring the tracking-store queue on the rig moved nothing: the DB-done -> Notify.Send
gap stayed at 108 ms. The cost was the audit half of CachedCallTelemetryForwarder:
SqliteAuditWriter.WriteAsync enqueues synchronously but returns the BATCH completion,
and the writer loop coalesces for FlushIntervalMs (50 ms) before it commits, so each
awaited audit row cost ~50 ms + fsync and a cached call awaited three (submit,
attempted, resolve) on the script thread. Every other script-path emitter already
fired-and-observed; only this forwarder awaited.

TryEmitAuditAsync now hands the row to the writer and observes the completion on a
fault-logging continuation (the FallbackAuditWriter ring-buffer fallback still runs
inside that task). Best-effort contract unchanged; per-store FIFO unchanged; nothing
joins the audit and tracking stores by commit order.

Rig, FlowOutboundDemo, 5-run medians: inbound 194 -> 25 ms, site script run 176 -> 17 ms,
CachedWrite submit->done 61 -> 11 ms, DB done -> Notify.Send 108 -> 0.1 ms. The tracking
queue stays (three fewer fsyncs per call, batched). Docs corrected to attribute the cost
to the flush wait, with the trap recorded in CLAUDE.md and Component-SiteRuntime.md.

Tests: ForwardAsync returns while the audit commit is still pending (never-completing
writer task) and still records tracking; a late commit fault is swallowed.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 12:35:52 -04:00
Joseph Doherty d91135f960 perf(site-runtime): operation tracking store writes off the script thread (single-writer queue, batch per transaction)
A Flow Debugger live run (execution 1130c01f) spent 108 of a 176 ms site script run
between the cached DB write completing and Notify.Send starting: the immediate-
completion telemetry awaited two autocommit SQLite transactions on the consolidated
LocalDb file (RecordAttemptAsync + RecordTerminalAsync), on the script thread, each a
WAL fsync on a Docker volume; the submit row was a third. The audit half of the same
forwarder was already a channel write, so the tracking half was the whole cost.

OperationTrackingStore is now a single-writer queue: Record*Async enqueue an ordered
work item and return; one background pump drains the bounded channel (8192, wait
backpressure - tracking never drops) and applies each batch (<=256) in ONE transaction,
falling back to per-row apply when a batch fails so a poison row cannot take its
neighbours. Preserved invariants: FIFO across enqueue/attempt/terminal; read-your-
writes (GetStatusAsync and ReadChangedSinceAsync barrier on every write queued before
them, skipped when idle) so Tracking.Status, the telemetry actor's status join and
central's PullSiteCalls never see a stale row; timestamps stamped when recorded;
PurgeTerminalAsync returns after the purge lands; DisposeAsync drains the queue before
closing; sync Dispose keeps its documented in-flight-writes-fail semantics. New public
FlushAsync() is the explicit barrier. Accepted: a queued-but-unapplied row is lost on a
hard crash (ms window); S&F buffer and audit rows are unaffected.

No interface, forwarder or ScriptRuntimeContext change. Tests: write returns while
another connection holds the DB write lock and lands once released (fails on the old
store: 31 s); reads see 100 ordered attempts + terminal; DisposeAsync drains 200
queued rows; write after dispose throws ObjectDisposed. Existing tests that poke the
SQLite file right after a store call now FlushAsync first.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 12:25:58 -04:00
Joseph Doherty 7caf56f332 fix(central-ui): Flow Debugger outcome refresh merges rows by EventId instead of replacing the graph
Found in a browser Live run on the rig: the run drew API, DB and Notify nodes from
its inline rows; the 5 s pending-refresh re-read the tree through Replay before the
site's cached-call telemetry (12 s) had reached central, swapped the graph wholesale,
and the resolved DB node vanished (7 -> 6 steps) until a manual refresh. Polling had
already stopped because nothing was Pending.

MergeRefreshedGraph now unions the re-read rows into the run's known rows by EventId
(fresh copy wins), stores the union back on _raw and rebuilds through
FlowGraphBuilder, so a refresh only ever adds nodes. The union is exact: the inline
rows are the same AuditEvent objects the site writer persists (FlowTraceCollector).

Tests: new PendingRefresh_KeepsStepsTheLiveRunAlreadyHad_WhenReplayLagsBehindTelemetry
(fails on the old code). Two existing fixtures were internally inconsistent and only
passed under the old replace semantics: ParkedThenResolved built its Resolved outcome
with the Parked raw rows, and OutboundOutcome minted fresh event/correlation ids per
call, so no row could ever match across live and replay. Both now share rows per run.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 11:55:52 -04:00
Joseph Doherty 91d14ebdee fix(central-ui): notification deep link and relay use the stored N-form NotificationId
Notify.Send mints NotificationId as Guid "N" (no dashes) and the outbox parses it back into
the audit row's CorrelationId, so the Flow Debugger's Notify node linked and relayed a "D"
string the Notifications table never matches (found live: the report opened empty). The
link and RetryNotificationRequest now use "N", and the Notification report normalises any
Guid-shaped ?correlationId= to "N" so Audit Log and Flow Debugger callers both land.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 11:16:24 -04:00
Joseph Doherty 1f8c5810ca test(central-ui): Playwright Flow Debugger live run + localStorage restore against the rig
An ephemeral inbound method (created and deleted through the CLI, with a never-taken
Route.To branch so prediction resolves a route target) is run live in a real browser through
the fixture's /auth/login; the reload test drives the OnAfterRenderAsync localStorage restore
and then proves the page is still interactive. Both fail on the pre-fix image at the
'Predicted' wait (the circuit deadlock) and pass on the fixed one.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 11:11:46 -04:00
Joseph Doherty 7f557b0ee5 feat(central-ui): Retry/Discard a parked outbound node from the Flow Debugger with scope check, confirm and re-poll
The page wires FlowStepDetail's OnRetry/OnDiscard to IFlowDebugService.RetryOutboundAsync /
DiscardOutboundAsync behind the same site-scope re-check and confirm wording the Site Calls /
Notification report pages use, toasts the relay outcome, and on success re-polls Replay (same
5 s / 2 min loop) until the acted node leaves Parked. Also drops a duplicated ?correlationId=
test block that a concurrent index-only commit left in QueryStringDrillInTests.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 11:08:05 -04:00
Joseph Doherty 33f9c97030 feat(central-ui): Site Calls page accepts ?correlationId= and opens the operation's detail
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 11:04:39 -04:00
Joseph Doherty 8abc0eca13 feat(central-ui): Notification report accepts ?correlationId= and opens the notification's detail
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 11:04:29 -04:00
Joseph Doherty 2025ec346f feat(central-ui): FlowDebugService relays Retry/Discard for a parked outbound step
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 11:04:03 -04:00
Joseph Doherty 9c31f70105 feat(central-ui): Flow Debugger step detail links to the report page and exposes Retry/Discard on a parked node
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 11:01:08 -04:00
Joseph Doherty 95773aa9fd feat(site-call-audit): additive TrackedOperationId query filter for ?correlationId= deep links
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 11:00:03 -04:00
Joseph Doherty 5497ffd181 feat(notification-outbox): additive NotificationId query filter for ?correlationId= deep links
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 10:59:49 -04:00
Joseph Doherty 7e3e809a70 feat(flow-debugger): FlowStep carries the audit CorrelationId; CLI table adds a correlation column
Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 10:58:50 -04:00
Joseph Doherty 4769e192e3 fix(central-ui): Flow Debugger prediction no longer deadlocks the Blazor circuit
FlowDebugService.PredictMethodAsync resolved route targets with a sync-over-async
GetAwaiter().GetResult() inside the predictor's synchronous resolver — on the circuit's
single-threaded renderer sync context that blocked the dispatcher forever the moment a
method whose script routes to a site script was chosen (every later click and JS-interop
reply queued behind it; the page looked inert with a healthy websocket and no error).
Misdiagnosed on 2026-09-06 as a login-disabled localStorage stall. Now a resolve-and-rewalk
loop: each pass records the targets it could not answer, they are awaited between passes,
and the walk converges once a pass asks for nothing new (bounded by MaxPredictionPasses).

Regression test drives the service under a single-thread pump sync context (fails in 10 s
on the old code, passes now) plus a nested Scripts.Call resolve-across-passes test.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 10:57:09 -04:00
Joseph Doherty 3c9f9da818 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
2026-09-06 02:10:50 -04:00
Joseph Doherty c3b4068de2 feat(central-ui): Flow Debugger refreshes pending outbound outcomes after a Live run
After a successful Live run whose graph has Pending outbound steps the page
re-reads the tree through the existing Replay command every 5 s for up to
2 min (page constants), merging by replacing the graph and keeping the
selection by event id; never in Replay mode; cancelled by a new run, a mode
switch or Dispose. Status strip gains a Pending count, a 'Refresh outcomes'
button (both modes) and an 'outcomes still arriving' note while polling. The
run's own timing and the Completed chip are left alone — pending outbound
work is not a failed run. Plan 2026-09-06 D5.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 02:03:16 -04:00
Joseph Doherty 8f6f7ed92b feat(central-ui): FlowStepDetail Lifecycle tab + outbound request/response labels
A folded outbound step shows a Lifecycle tab (time · kind · status · node ·
error, oldest first; a Parked step adds the 'retry or discard from Site Calls /
Notification report' hint), replaces the never-populated Writes tab for those
steps, labels the Input tab Request / SQL + parameters / Subject + body by kind,
and 'Where this ran' names the System / Connection / List plus 'Delivered by'
(the terminal row's node). Pending/Parked outcome badges.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 01:59:55 -04:00
Joseph Doherty 56154fd1d2 feat(central-ui): FlowGraphView External lane, outbound status pills, async/spawn connectors
Four lanes (LaneCount 4; External sublabel = distinct targets, three then +n);
ApiCall/DbCall/Notify nodes carry a status pill (Ok/Pending/Parked/Failed —
pending pulses, reduced-motion aware) plus a sync/cached/notify mode; folded
lifecycles show '+4.1 s' submit→terminal wall time. Async edges are dashed
and routed through the inter-lane strip with a gutter tag; spawn edges use a
bolt marker. Predicted async edges (Task 6's PredictedEdge.Async) render the
same way. de-DE height pinned at 319.2 = (4×110+16)×0.7.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 01:57:47 -04:00
Joseph Doherty ec9011429c fix(site-runtime): trace sink is guarded — a throwing observer can neither fail a call nor skip the audit write
Review of Task 3: the sink invoke sat before WriteAsync/ForwardAsync inside the
best-effort try, so a throwing sink would have silently dropped the durable
row. GuardTraceSink wraps the delegate in every helper ctor (and Collect
guards EmitTrace); pinned by a throwing-sink test. ScriptRunLauncher no longer
allocates a collector for a traced run with no reply target (Nobody) — the
list would never be read. FlowTraceCollector.Complete pinned at unit level;
Snapshot documented as a non-closing peek.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 01:57:09 -04:00
Joseph Doherty 69feb40e65 feat(inbound-api): audit central method-script Database.* calls (DbOutbound) and feed the flow-trace sink
InboundDatabaseHelper gains trailing optional auditWriter / nodeIdentity /
executionId / parentExecutionId / methodName / traceSink parameters; every
QuerySingle/Query/Execute writes one DbOutbound/DbWrite row (Delivered/Failed,
Target = connection name, Actor = 'api:{method}', DurationMs, SQL + parameters
as RequestSummary, Extra in the site's {op, rowsAffected|rowsReturned}
vocabulary, SourceNode) via ICentralAuditWriter — best-effort, never into the
script; the same instance goes to the RouteHelper trace sink first. Closes the
audit-scope gap Component-AuditLog.md already claimed (plan D7). Redaction and
the 8 KB cap apply at write time in the shared redactor, as for site rows.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 01:52:50 -04:00
Joseph Doherty 3b9def5a09 feat(script-analysis): predict outbound calls as ApiCall/DbCall/Notify on the External lane
FlowShapePredictor emits ExternalSystem.Call/CachedCall → ApiCall ('{s}.{m}'),
Database.Execute/Query/QuerySingle/CachedWrite → DbCall ('{conn} · {member}'),
Notify.Send(list) and Notify.To(list).Send() → Notify ('Notify {list}'), all on
FlowLane.External; Async-suffixed spellings map identically. PredictedEdge gains
an additive Async flag; an async edge (cached call, notify) does not advance the
chain, mirroring FlowGraphBuilder.IsAsync (D4/D8). Database.Connection() raw
handles no longer predict a node (command count is not statically knowable).

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 01:51:42 -04:00
Joseph Doherty f394f58405 feat(site-runtime): outbound API/DB/notification audit rows ride the flow-trace collector
ScriptRuntimeContext caches a trace sink (null on an untraced run, so the
production path is byte-identical) and threads it into the ExternalSystem /
Database / Notify helpers and AuditingDbConnection→AuditingDbCommand (onAudit).
All eight outbound emit sites hand the SAME AuditEvent instance to the sink
first, then to the writer/forwarder. FlowTraceCollector gains Completed +
Complete(): ScriptRunLauncher closes the collector where it builds the reply
snapshot, so un-awaited calls finishing after the run returned never mutate a
list the reply was built from. Cap (500) unchanged. Plan 2026-09-06 D5.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 01:51:19 -04:00
Joseph Doherty 865c4be651 fix(commons): FlowGraphBuilder review fixes — leading async steps link from the spawner, robust async rule, lifecycle tie-break, total DbSubtitle
Review of Task 2 (2026-09-06 outbound-calls plan): (1) an outbound step that
precedes the first chain step of its execution — the default Replay shape when
a run's ScriptTrace rows are absent — was orphaned or given a solid spawner
edge; leading async steps now hang off the spawner on Async edges and the
first non-async step gets the normal link. (2) IsAsync keyed on the first row
being CachedSubmit; it now mirrors the projector's sync rule (first row not
ApiCall/DbWrite) so a group missing its submit row still branches. (3) Same-
tick rows tie-broke by GUID; new LifecycleRank (submit < attempt < resolve,
non-terminal < terminal) makes first/latest attribution order-independent.
(4) DbSubtitle used GetInt32 (FormatException on out-of-range/fractional) —
now TryGetInt32, never throws. Also: External-lane edges never count as
central↔site crossings; DurationMs clamped; FlowStep doc updated.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 01:49:40 -04:00
Joseph Doherty 30faee0d0e feat(audit): replay/pull include outbound API, DB and notification rows
GetTraceEventsAsync widens its channel filter to ScriptTrace ∪ ApiOutbound ∪
DbOutbound ∪ Notification ∪ (ApiInbound & InboundRequest) (plan D6); SecuredWrite,
Cluster and InboundAuthFailure rows in the same tree stay excluded. The site
SqliteAuditWriter.ReadByExecutionIdsAsync already had no channel predicate — pinned
by a test. Interface doc updated to match.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 01:47:05 -04:00
Joseph Doherty 29129ef17a feat(commons): FlowGraphBuilder folds outbound lifecycles into one node; async/spawn edges; Response from the inbound execution
Fold rule: outbound rows sharing (kind, CorrelationId) become one FlowStep with
a Lifecycle list — first row's identity/time/input, latest row's outcome/
subtitle/output/error, DurationMs = sync row's own or submit→terminal wall time
(null while pending). Chain skips async steps (Notify, or first row
CachedSubmit) and links them with Async edges from the preceding chain step.
Spawner edges from a non-RouteCall are Spawn: true. The Response links from the
last non-async step of the inbound execution (the RouteCall), not the globally
last cascaded step (D9). FlowGraphView places a Response at max column + 1 so
the D9 origin does not pull it left (pulled forward from Task 7 to keep the
layout test green).

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 01:42:48 -04:00
Joseph Doherty e26616bebc feat(commons): project outbound API/DB/notification rows onto the External lane
ResolveKind maps ApiOutbound/DbOutbound/Notification to ApiCall/DbCall/Notify;
ResolveLane puts the three on FlowLane.External; outbound outcomes map
Delivered→Ok, Failed→Failed, Parked/Discarded→Parked, else Pending; subtitles
'{status} · sync|cached', '{op} · n rows' from a DbWrite Extra, '{status}' for
Notify. New FlowStepProjector.IsOutbound helper.

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
2026-09-06 01:38:41 -04:00
Joseph Doherty cc1bbf3975 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
2026-09-06 01:37:29 -04:00
Joseph Doherty 038eda5d91 fix(central-ui): Flow Debugger node text readability — HTML node bodies, shared-prefix gutter, wrap hints
The swimlane's node text was clipped by character count (20/24), so dotted attribute
paths showed only their shared head ("DelmiaReceiver.Reci…" five times in a row) and
the subtitle spilled past the box. It was also unstyled: the SVG <text> elements are
emitted as raw markup (Razor reserves the <text> tag), so they never carried the CSS
isolation scope attribute and every .fg-* rule silently missed.

- Node body is now HTML inside an SVG foreignObject: the browser measures the text,
  the title wraps to two lines and ellipsizes by width, the subtitle by width on one
  line, nothing can overflow the box, and the div's title attribute carries the full
  unstripped text as a tooltip. Node height 78 -> 84.
- A lane whose real titles share a dotted head (CommonDottedPrefix) shows it once in
  the lane gutter ("DelmiaReceiver.*") and each node shows only the tail that tells
  it apart; the write subtitle strips the same head per item. Predicted-only nodes
  are never stripped.
- WrapHints inserts zero-width spaces at camel-case and ./_/ boundaries so a wrapped
  title breaks at "DelmiaRecipe|Download" rather than mid-word.
- Gutter widened 130 -> 160 with the sublabel clip raised to 22 so "site-a · DelmiaSim01"
  fits; the remaining SvgText gutter labels are styled through ::deep.

Verified live on the rig (replay of the Task 22 run): all three lanes readable, no
overflow, gutter shows the factored prefix. CentralUI suite 1199 pass.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-06 01:15:56 -04:00
Joseph Doherty 66d5a67e4f feat(central-ui): Flow Debugger drill-ins from Audit Log + Execution Chain; TraceAllCalls checkbox on the method form
'Open in Flow Debugger' (data-test open-flow-debugger) beside 'View execution chain' in
AuditEventDetail (rendered only with an ExecutionId) and beside 'View this execution in
the Audit Log' on the Execution Chain page — both target
/deployment/flow-debugger?executionId=… which replays on first render. ApiMethodForm gains a
'Trace all calls (Flow Debugger)' form-check under Timeout, loaded from and saved to
ApiMethod.TraceAllCalls on both the update and create paths, with the ~7-rows-per-call
guidance as help text.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-06 00:33:37 -04:00
Joseph Doherty 254118a467 feat(central-ui): Flow Debugger page (predict, live run, replay, step detail) + nav entry
/deployment/flow-debugger (RequireDeployment): entry toggle (inbound method | value-change
trigger), method picker or Site → Instance → Attribute pickers (site-scope filtered), Live |
Replay mode, inputs form generated from the method's ParameterDefinitions via
InboundApiSchema (values coerced by declared type into the request JsonElement), status
strip (state chip, run #, start, total, steps/boundaries/writes, root ExecutionId link to
the Execution Chain page), the FlowGraphView with −/Fit/+ zoom, and FlowStepDetail (tabs
Input · Output · Writes · Timing, 'Where this ran' column, Open in Debug View when the
instance is known, Audit rows (n) → /audit/log?executionId=). State machine Idle →
Predicted → Running → Completed | Failed | TimedOut; STANDBY_NODE renders as an inline
warning alert with the server message verbatim; ?methodId= / ?instanceId=&attribute= /
?executionId= drill-ins (the last replays on first render); last entry + inputs persisted
per method in localStorage; 'site not traced' label when a run's route call came back with
no child execution and no Site-lane steps. NavMenu: Deployment → Flow Debugger.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-06 00:31:55 -04:00
Joseph Doherty ba15a38327 feat(central-ui): FlowGraphView SVG swimlane renderer
Dependency-free inline SVG: three fixed 110px lanes (Central / Site / Tags, sublabelled
from the steps' site/instance), columns from a topological pass over the graph edges with
same-lane collision bumping so independent siblings never overlap, 150x78 nodes on a
130+col*170 grid, orthogonal connectors with arrow markers (accent for boundary crossings,
dashed for external actors, light for predicted edges). Renders a real FlowGraph, a
PredictedFlow (dashed nodes, input-dependent finer-dashed), or both — predicted nodes
matched to real steps by kind in order, the leftovers appended greyed as ghosts. Selected
node = accent 2px on --active-bg; failed/timed-out/not-traced outcomes styled; each node a
role=button tabindex=0 <g> with click + Enter/Space selection; svg role=img with a summary
aria-label. Layout is a pure static ComputeLayout, every coordinate InvariantCulture.
Razor reserves the <text> tag, so SVG text elements are emitted through a small
HTML-encoding MarkupString helper.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-06 00:25:54 -04:00
Joseph Doherty f9b8a2cf8f feat(central-ui): FlowDebugService — management-command bridge + predicted graph
IFlowDebugService/FlowDebugService (scoped, registered beside IDataConnectionMoveService).
Run/replay go to the per-node ManagementActor through ManagementActorHolder with the Blazor
principal projected to AuthenticatedUser, exactly as DataConnectionMoveService does; the
FlowDebugRunResult payload is deserialised (camelCase) and fed to FlowGraphBuilder, while
ManagementError/Unauthorized/holder-null/transport faults collapse into FlowDebugOutcome
with the server's error code verbatim (STANDBY_NODE reaches the page untouched). Prediction
is DB-only: PredictMethodAsync hands FlowShapePredictor a memoised resolver that maps a
route target → Instance.UniqueName → template parent chain (nearest definition wins, cycle
guarded) → TemplateScript.Code; PredictValueChangeAsync roots at the write and appends each
script whose value-change trigger names the attribute (same attributeName/attribute keys
the site's TriggerRouting parser reads).

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-06 00:20:22 -04:00
Joseph Doherty b73d6b5e2e feat(script-analysis): FlowShapePredictor — predicted flow graph from script syntax
Pure Roslyn syntax walk (SourceCodeKind.Script, no semantic model, DB-free) that turns a
method or site script into PredictedNode/PredictedEdge chains in source order: Route.To(x)
.Call/SetAttributes/WaitForAttribute, Scripts.Call, Attributes.WriteBatchAndWaitAsync/
WaitAsync/WaitForAsync/SetAsync, Attributes[x] = v, ExternalSystem/Database/Notify. String
literals resolve; anything else is InputDependent with the expression text as title. An
if-return guard becomes a note on the node that follows it (condition visited first, so a
guard whose condition IS the traced call — Delmia's WriteBatchAndWaitAsync — attributes
correctly). Routed/nested script bodies come from a caller-supplied resolver, recursion
capped by maxDepth; parse errors and any walker fault degrade to request+response with a
'parse error' note. Predicted kinds are exactly the traced kinds so the page can overlay by
kind + title. Deviation from the plan's node count: no separate central 'method script'
node — the real graph records none, so the InboundRequest node stands for it.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-06 00:15:42 -04:00
Joseph Doherty 0fb7e16ced feat(cli): flow-debug run-method|run-value-change|replay
New top-level command group for the Flow Debugger (design 2026-09-05 §7.1). run-method /
run-value-change post the FlowDebugRun* management commands with a 2-minute client wait;
replay posts FlowDebugReplayCommand. A table projection unwraps the FlowDebugRunResult
envelope to one row per step (occurred | kind | lane | target | durationMs | status |
error) using the Commons FlowStepProjector's kind/lane resolution, with a trailing note
row for an incomplete run (error code verbatim, e.g. STANDBY_NODE) or a truncated event
list. --params must be a JSON object (INVALID_PARAMS otherwise). README section added;
command-tree test bumped to 18 groups.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-06 00:11:42 -04:00
Joseph Doherty d11dcb77c5 feat(management): FlowDebug run-method / run-value-change / replay commands (Deployer, active-node gated, inline trace)
Flow Debugger (design 2026-09-05 §7.1): FlowDebugRunMethodCommand / FlowDebugRunValueChangeCommand /
FlowDebugReplayCommand on the per-node ManagementActor (Deployer role; the two run commands
are long-running), all delegating to FlowDebugRunner. RunMethod refuses on a standby with
STANDBY_NODE (ManagementCommandException now carries an optional Code that replaces
COMMAND_FAILED), validates parameters through the real ParameterValidator/SchemaRefResolver
(made public), synthesizes the inbound row (actor = operator, Extra.source = flow-debugger)
and runs the method through InboundScriptExecutor with a trace-sink RouteHelper so the reply
carries the RouteCall row + the site's inline events. RunValueChange does a traced routed
write via IInstanceRouter, then polls IPullTraceEventsClient (interface moved to Commons so
ManagementService can consume it) until a ScriptRun parented to the root settles, else
NO_TRIGGER_WITHIN_WINDOW. Replay reads IAuditLogRepository.GetTraceEventsAsync.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-06 00:04:18 -04:00
Joseph Doherty 9300bc1a4f feat(audit): GetTraceEventsAsync returns the ScriptTrace chain for Replay
Flow Debugger (design 2026-09-05 §7.1): IAuditLogRepository.GetTraceEventsAsync walks any
entry id to its root and down the Edges/Chain CTE exactly as GetExecutionTreeAsync, then
returns the chain's ScriptTrace rows plus the ApiInbound.InboundRequest row via the canonical
row mapper, ordered (OccurredAtUtc, EventId). Test doubles implement the new member with a
NotSupported stub.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-05 23:59:08 -04:00
Joseph Doherty 981deed0fd feat(inbound-api): executor threads the trace flag; endpoint honours ApiMethod.TraceAllCalls
Flow Debugger (design 2026-09-05 §5.2): InboundScriptExecutor.ExecuteAsync gains a trailing
trace parameter that builds the script's Route helper WithTrace(trace); the inbound endpoint
passes method.TraceAllCalls so a flagged integration method is traced on every real call.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-05 23:59:08 -04:00
Joseph Doherty 3f582a278c feat(inbound-api): RouteHelper writes an always-on RouteCall audit row, propagates Trace, exposes an inline trace sink
Flow Debugger (design 2026-09-05 §5.1/§5.2/§7.1): RouteTarget.Call emits one
ScriptTrace.RouteCall central-direct-write row per routed call (ExecutionId = the inbound
parent, Extra.childExecutionId from the reply, best-effort, never throws into the script)
whether or not the run is traced. WithTrace(bool) stamps Trace on RouteToCallRequest and
RouteToSetAttributesRequest; WithTraceSink hands the row + the reply's inline site events to a
callback so a Live run renders without touching the store. DI supplies ICentralAuditWriter /
INodeIdentityProvider as optional ctor args; new RouteHelper(locator, router) still compiles.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-05 23:59:08 -04:00
Joseph Doherty 7e61552d82 feat(wire): attribute updates carry source_execution_id/trace; PullTraceEvents RPC + site read-by-execution-ids
Flow Debugger (design 2026-09-05 §5.3/§5.4): AttributeValueUpdate fields 7/8 stamped by
StreamRelayActor and parsed by SiteStreamGrpcClient (older frames => null/false). New
PullTraceEvents unary RPC on SiteStreamService (same PSK-gated prefix as PullAuditEvents):
a point read of the site audit SQLite by ExecutionId OR ParentExecutionId
(ISiteAuditQueue.ReadByExecutionIdsAsync, json_extract on the codec's camelCase keys, new
IX_audit_event_occurred index, 64-id / 2000-row caps) that moves no cursor and flips no
ForwardState. Central IPullTraceEventsClient/GrpcPullTraceEventsClient mirror the audit pull
client (endpoint resolution, NodeB failover, fault => empty) and register beside it.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-05 23:52:50 -04:00
Joseph Doherty f15d0db0ad feat(wire): route_to_call trace flag + child_execution_id + trace_events; route_to_set_attributes trace (additive proto + mapper)
Flow Debugger (design 2026-09-05 §5.2/§5.4): site_command.proto gains trace=7 on
RouteToCallRequestDto, child_execution_id=6 + repeated sitestream.AuditEventDto trace_events=7
on RouteToCallResponseDto, trace=6 on RouteToSetAttributesRequestDto (imports
Protos/sitestream.proto the way central_control.proto does). Mapper round-trips them; an
empty trace_events list maps to null so an untraced/older site reads as 'not traced'.
Vendored C# regenerated via docker/regen-proto.sh (csproj untouched); golden samples extended.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-05 23:52:50 -04:00
Joseph Doherty 2c86adb782 feat(site-runtime): route handlers forward Trace and return child ExecutionId + trace events
Flow Debugger (design 2026-09-05 §5.2/§5.4): RouteInboundApiCall passes Trace on the
ScriptCallRequest and maps ScriptCallResult.ExecutionId/TraceEvents onto
RouteToCallResponse; RouteInboundApiSetAttributes passes Trace on SetStaticAttributeCommand.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-05 23:43:33 -04:00
Joseph Doherty 01be39019e feat(site-runtime): InstanceActor trace arm stamps the device echo of a traced write; Trace rides on published changes
Flow Debugger (design 2026-09-05 §5.3): a traced SetStaticAttributeCommand on a data-sourced
attribute records (attribute → writer, expiry) for TraceArmWindowSeconds (new option,
default 30, validated > 0); the next DCL echo for that attribute is published with the
writer's SourceExecutionId + Trace whatever its value, and the arm is consumed once. Static
writes publish Trace directly. Untraced writes never arm (pinned).

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-05 23:43:33 -04:00
Joseph Doherty eca1434160 feat(site-runtime): traced runs emit ScriptRun rows, return ExecutionId + trace events; traced value-change runs inherit the parent
Flow Debugger (design 2026-09-05 §5.1/§5.4): ScriptRunLauncher emits one ScriptRun row per
traced run on every exit path (OccurredAtUtc = run start), replies with the run's
ExecutionId (always) and the collector snapshot (traced only). ScriptActor forwards
ScriptCallRequest.Trace and, for a value-change trigger fired by a traced stamped change,
parents the run to the writer with trigger detail in Extra; every other trigger stays a root.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-05 23:43:33 -04:00
Joseph Doherty 93489cd7a2 feat(site-runtime): ScriptRuntimeContext emits ScriptTrace AttributeWrite/AttributeWait rows and inherits the Trace flag
Flow Debugger (design 2026-09-05 §5.1/§5.2): the context is the single funnel for
SetAttribute / WriteBatchAndWait / WaitInternal, so it emits the write/wait rows (collector
first, then the best-effort IAuditWriter whose redactor applies the payload cap), stamps
Trace on SetStaticAttributeCommand and ScriptCallRequest, and hands the flag + sink to the
inline CallShared child context. Untraced contexts emit nothing (pinned).

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-05 23:43:32 -04:00
Joseph Doherty 23157d2bcf feat(inbound-api): ApiMethod.TraceAllCalls (entity, migration, mgmt command, CLI --trace-all-calls)
Flow Debugger (design 2026-09-05 §5.2): bit NOT NULL DEFAULT 0 column + EF migration
20260906034113 with its docs/plans/sql production twin. CreateApiMethodCommand gains
TraceAllCalls=false; UpdateApiMethodCommand gains bool? TraceAllCalls (null = unchanged so an
older CLI never resets it). Management result echoes the flag; CLI create/update expose
--trace-all-calls; README carries the ~7-rows-per-call warning.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-05 23:42:52 -04:00
Joseph Doherty febb268d89 feat(site-runtime): FlowTraceCollector
Per-run inline sink for ScriptTrace audit events (Flow Debugger design 2026-09-05 §5.4):
cap 500, Truncated flag, lock-based; the audit rows stay complete beyond the cap.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-05 23:42:39 -04:00
Joseph Doherty ec7b5b2ea3 feat(commons): FlowStep/FlowGraph projection + FlowGraphBuilder for the Flow Debugger
Central-side projection of ScriptTrace/ApiInbound audit events (design §7.2): lanes
Central/Site/Tags, in-execution chaining, spawner→child edges (RouteCall by
childExecutionId), inferred write→satisfied-wait echo edge, synthesized Response node.
ScriptTraceExtra is the camelCase Extra payload shared by every emitter.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-05 23:41:50 -04:00
Joseph Doherty 24e595d6d9 feat(commons): additive Trace flag + ChildExecutionId/TraceEvents on route/call/write/change records
Flow Debugger (design 2026-09-05 §5.2/§5.4): Trace rides beside ParentExecutionId on
RouteToCallRequest/RouteToSetAttributesRequest/ScriptCallRequest/SetStaticAttributeCommand/
AttributeValueChanged; RouteToCallResponse and ScriptCallResult carry the site run's
ExecutionId and its ScriptTrace AuditEvents inline. All trailing optional — zero call-site edits.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-05 23:41:50 -04:00
Joseph Doherty 352e0bfa37 feat(audit): ScriptTrace channel + RouteCall/ScriptRun/AttributeWrite/AttributeWait kinds
Flow Debugger (design 2026-09-05 §5.1): trace steps are audit rows. New
AuditChannel.ScriptTrace appended after Cluster and four kinds appended after
ManualFailover so stored ordinals never move; lock-in tests + alog.md §4 updated.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
2026-09-05 23:41:50 -04:00