docs(plans): Flow Debugger script entry point — design (site/instance/script; write kinds vs direct call)
Replaces the value-change (site/instance/attribute) entry with a script picker. ValueChange/Conditional scripts keep the write-and-poll path with the attribute derived from the trigger config; every other kind is invoked directly through the traced RouteToCall path with a parameter form from the script's ParameterDefinitions. New additive FlowDebugRunScriptCommand; run-value-change stays as the CLI escape hatch. Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
# Flow Debugger — Script Entry Point (design)
|
||||
|
||||
**Date:** 2026-09-08
|
||||
**Status:** approved (brainstorm 2026-09-08), implementation plan follows
|
||||
**Supersedes:** the *Value-change trigger* entry of `2026-09-05-flow-debugger-design.md` §4/§5.3 as the
|
||||
page's second entry point. §5.3's trace arm and the `FlowDebugRunValueChangeCommand` wire record are
|
||||
unchanged.
|
||||
|
||||
## 1. What changes
|
||||
|
||||
The Flow Debugger's second entry point selects **site → instance → script** instead of
|
||||
site → instance → attribute.
|
||||
|
||||
- A script whose trigger is **ValueChange or Conditional** names one monitored attribute in its
|
||||
trigger configuration. The debugger writes that attribute (the operator supplies only the new
|
||||
value) and waits for the run the change spawns — exactly today's value-change behaviour, with the
|
||||
attribute derived from the script rather than picked.
|
||||
- Every other script (**Call, Interval, Expression, or no trigger**) is invoked **directly** through
|
||||
the traced routed-call path that `Route.To(instance).Call(script, args)` already uses. The Inputs
|
||||
card shows a parameter form generated from the script's own `ParameterDefinitions`, the same way
|
||||
the inbound-API method entry does.
|
||||
|
||||
The decision on trigger kinds was taken in the brainstorm: Conditional joins ValueChange on the
|
||||
write side because both name a single attribute; Expression joins the call side because it names
|
||||
none, and a direct call is the only honest single-step run of it. The attribute-level CLI command
|
||||
`flow-debug run-value-change` stays as the escape hatch for firing an Expression trigger through a
|
||||
real attribute write.
|
||||
|
||||
## 2. Script resolution
|
||||
|
||||
Both the page and the runner resolve an instance's scripts through the existing
|
||||
`IFlatteningPipeline.FlattenAndValidateAsync(instanceId, validateScripts: false)` (DeploymentManager,
|
||||
already referenced by both CentralUI and ManagementService — Create Instance's binding preview uses
|
||||
the same pipeline). `FlattenedConfiguration.Scripts` gives `ResolvedScript` rows carrying the
|
||||
**canonical name** (so composed-module scripts appear as `Module.Script`, the name the site's
|
||||
`InstanceActor` keys on), the effective `TriggerType`/`TriggerConfiguration`, and the effective
|
||||
`ParameterDefinitions`/`ExecutionTimeoutSeconds` after inheritance and instance overrides.
|
||||
|
||||
The runner resolves the script itself and never trusts the client's claim of trigger kind: the
|
||||
branch below is taken on the server-side `ResolvedScript`.
|
||||
|
||||
A new pure Commons helper, `ScriptTriggerAttribute.TryRead(triggerType, configJson, out attribute)`
|
||||
(`Commons/Types/Scripts/`), returns the monitored attribute for `ValueChange`/`Conditional`
|
||||
(keys `attributeName` then `attribute`, matching `ScriptTriggerConfigCodec` and SiteRuntime's
|
||||
`TriggerRouting.TryReadAttributeName`) and false for every other kind. `FlowDebugService.IsValueChangeOn`
|
||||
is rewritten on top of it. SiteRuntime's private copy is left alone — it also serves alarms and
|
||||
fail-open routing, a different contract.
|
||||
|
||||
**Design vs deployed.** The site runs the last deployed snapshot. A script that exists in design but
|
||||
was never deployed fails at the site with its existing `Script 'X' not found on instance 'Y'` error,
|
||||
which the debugger surfaces verbatim; the page does not try to detect staleness (the Instances page
|
||||
already shows it).
|
||||
|
||||
## 3. Wire and command changes (additive)
|
||||
|
||||
```csharp
|
||||
/// Flow Debugger Live run of a site script by name (design 2026-09-08).
|
||||
public record FlowDebugRunScriptCommand(
|
||||
int InstanceId,
|
||||
string ScriptName, // canonical name from the flattened configuration
|
||||
JsonElement? Parameters, // call kinds: flat JSON object validated against the script's ParameterDefinitions
|
||||
string? Value, // write kinds: the new value for the trigger's attribute
|
||||
int WaitSeconds = 45); // write kinds: NO_TRIGGER_WITHIN_WINDOW window
|
||||
```
|
||||
|
||||
- Registered in `ManagementEndpoints`' command type list beside the other two run commands.
|
||||
- `ManagementActor.GetRequiredRoles`: `DeployerOnly`, same as `FlowDebugRunMethodCommand` /
|
||||
`FlowDebugRunValueChangeCommand`. Replay roles unchanged.
|
||||
- `FlowDebugRunValueChangeCommand` is **kept** (CLI `run-value-change`, additive-only contracts).
|
||||
`FlowDebugRunResult` is unchanged.
|
||||
- No proto change: the direct call rides the existing `RouteToCallRequest.Trace` /
|
||||
`RouteToCallResponse.TraceEvents` fields.
|
||||
|
||||
## 4. Runner (`FlowDebugRunner.RunScriptAsync`)
|
||||
|
||||
1. `EnsureActiveNode`; load instance (`NOT_FOUND`); `EnforceSiteScope(user, instance.SiteId)`; load site.
|
||||
2. Flatten; find `ResolvedScript` by canonical name, case-insensitive (`NOT_FOUND` with the message
|
||||
`Script 'X' is not defined on instance 'Y'.`).
|
||||
3. **Write kinds** (`ScriptTriggerAttribute.TryRead` succeeds): `Value` is required
|
||||
(`VALIDATION_FAILED`); call the shared private helper `WriteAndPollAsync(sp, user, instance, site,
|
||||
attribute, value, waitSeconds, ct)` — the body of today's `RunValueChangeAsync` from the traced
|
||||
`RouteToSetAttributesAsync` onward, extracted unchanged. `RunValueChangeAsync` becomes a thin caller
|
||||
of the same helper so both commands share one code path and one set of tests.
|
||||
4. **Call kinds**: `ParameterValidator.Validate(cmd.Parameters, script.ParameterDefinitions, resolveRef)`
|
||||
with `SchemaRefResolver.BuildAsync` over the shared-schema repository, exactly as `RunMethodAsync`
|
||||
(`VALIDATION_FAILED` on failure; null definitions + null parameters is valid and sends no
|
||||
parameters). Then:
|
||||
- `root = Guid.NewGuid()`; `timeout = script.ExecutionTimeoutSeconds ?? SiteRuntime default 30 s`
|
||||
(the runner has no site options — it uses a `DefaultScriptTimeoutSeconds = 30` constant, documented
|
||||
as mirroring `SiteRuntimeOptions.ScriptExecutionTimeoutSeconds`).
|
||||
- `IInstanceRouter.RouteToCallAsync(site.SiteIdentifier, new RouteToCallRequest(correlationId,
|
||||
instance.UniqueName, script.CanonicalName, validation.Parameters, now, ParentExecutionId: root,
|
||||
Trace: true), linked token cancelled after timeout + RunGrace)`. A cancellation is reported as
|
||||
`TIMEOUT` like the method path.
|
||||
- Synthesize the **root row**: `AuditChannel.ScriptTrace` / `AuditKind.RouteCall`, status by
|
||||
`response.Success`, actor = operator, target = `{instance}.{script}`, `executionId = root`,
|
||||
`sourceSiteId`/`sourceInstanceId`, `durationMs`, `requestSummary` = the parameters JSON,
|
||||
`responseSummary` = the return value serialized with `ScriptTraceExtra.JsonOptions`,
|
||||
`errorMessage`, `extra = ScriptTraceExtra { Source = "flow-debugger" }`. Written through
|
||||
`WriteCentralAsync`; the returned copy is `redactor.Apply`'d (review follow-up rule).
|
||||
- The site's `response.TraceEvents` are pushed through a `FlowTraceCollector(redactor:)` so the
|
||||
inline copy is redacted and capped (arch-review F1), then `Complete()`d.
|
||||
- Result: `[redactedRoot] + collector events`, in **emission order** (F5: no re-sort across clocks),
|
||||
`Completed = response.Success`, `ErrorCode = response.Success ? null : "CALL_FAILED"`,
|
||||
`Truncated = collector.Truncated`.
|
||||
|
||||
Not using `RouteHelper` here is deliberate: it would emit its own always-on `RouteCall` row with no
|
||||
operator actor, giving the graph two roots. Calling `IInstanceRouter` directly mirrors how the
|
||||
write path calls `RouteToSetAttributesAsync` and keeps one synthesized, operator-attributed root.
|
||||
|
||||
`FlowGraphBuilder` already treats the first row of the root execution as the chain root; a
|
||||
`RouteCall`-kind root with `ScriptRun` children parented to it renders Central → Site exactly like the
|
||||
routed leg of a method run. The plan pins this with a builder test.
|
||||
|
||||
## 5. Central UI
|
||||
|
||||
**`IFlowDebugService`** (UI-internal, not a wire contract):
|
||||
|
||||
```csharp
|
||||
sealed record FlowScriptOption(string CanonicalName, string? TriggerType, string? TriggerAttribute,
|
||||
string? ParameterDefinitions, bool IsWriteKind);
|
||||
Task<IReadOnlyList<FlowScriptOption>> GetInstanceScriptsAsync(int instanceId, CancellationToken ct = default);
|
||||
Task<PredictedFlow> PredictScriptAsync(int instanceId, string scriptName, CancellationToken ct = default);
|
||||
Task<FlowDebugOutcome> RunScriptAsync(int instanceId, string scriptName, JsonElement? parameters,
|
||||
string? value, CancellationToken ct = default);
|
||||
```
|
||||
|
||||
`PredictValueChangeAsync` / `RunValueChangeAsync` are removed from the UI service (their only caller
|
||||
was the page); `PredictScriptAsync` delegates to the value-change prediction for write kinds (with
|
||||
the trigger's attribute) and, for call kinds, builds a `RouteCall` root on the Central lane
|
||||
(`{instance}.{script}`) followed by `FlowShapePredictor.PredictSiteScript`. `RunScriptAsync`'s Ask
|
||||
timeout is `RunAskTimeoutFor(script.ExecutionTimeoutSeconds ?? 30)` for call kinds and
|
||||
`ValueChangeAskTimeoutFor(45)` for write kinds. The script list is cached per instance for the life
|
||||
of the scoped service (one flatten per selection).
|
||||
|
||||
**Page (`FlowDebugger.razor(.cs)`).**
|
||||
|
||||
- Entry button label: **Site script** (`data-test="flow-entry-script"`, replacing
|
||||
`flow-entry-value-change`; `EntryKind.ValueChange` → `EntryKind.Script`).
|
||||
- Pickers: Site, Instance, **Script** (`data-test="flow-script-select"`); each option renders
|
||||
`Name (ValueChange: Level)` / `(Conditional: Level)` / `(Call)` / `(Interval)` / `(Expression)` /
|
||||
`(no trigger)`.
|
||||
- Inputs card: write kinds → the existing single field, labelled *New value for `<attribute>`* with
|
||||
the attribute read-only (`flow-input-value`); call kinds → the shared `ParameterValueForm` bound to
|
||||
the option's `ParameterDefinitions` (`data-test="flow-script-params"`), or the note *This script
|
||||
takes no parameters* when the definitions are empty. `ParameterValueForm` hands back
|
||||
`Dictionary<string, object?>`, serialized to a `JsonElement` for the command.
|
||||
- `CanRun`: script chosen and (write kind → any value entered; call kind → form has no parse errors).
|
||||
- Query parameters: `?instanceId=` and `?script=`; `?attribute=` is removed (nothing outside the page
|
||||
links with it). Inputs persistence keys become `flow-debugger.script.{instanceId}.{script}`.
|
||||
- `FlowStepDetail`'s `SiteDbId`/`InstanceDbId` pass-through keys on `EntryKind.Script`.
|
||||
- The status strip, refresh loop, Retry/Discard and Replay are untouched.
|
||||
|
||||
## 6. CLI
|
||||
|
||||
`flow-debug run-script --instance-id <id> --script <canonical name> [--params '<json>'] [--value <v>]
|
||||
[--wait-seconds 45]` → `FlowDebugRunScriptCommand`, same client timeout rule as `run-method`
|
||||
(`--timeout-seconds`, default 120) because the server may run a call script up to its execution
|
||||
timeout + grace. `run-value-change` stays, its help text noting it is the attribute-level path (fires
|
||||
Expression triggers) and that `run-script` is the script-level one.
|
||||
|
||||
## 7. Security and audit
|
||||
|
||||
Unchanged posture: Deployer-only, active node only, site scope enforced from the instance before any
|
||||
site traffic, the root row is written centrally against the operator's name whether the call
|
||||
succeeded or not, inline trace copies are redacted and capped. A direct call of a script is a real
|
||||
run with real side effects — the page's existing Live-run wording already says so.
|
||||
|
||||
## 8. Tests
|
||||
|
||||
- `FlowDebugRunnerTests`: write-kind script → `RouteToSetAttributesAsync` with the trigger's attribute;
|
||||
call-kind script → `RouteToCallAsync` with `Trace: true`, parent = root, parameters validated;
|
||||
unknown script → `NOT_FOUND`; missing `Value` on a write kind → `VALIDATION_FAILED`; bad parameters
|
||||
→ `VALIDATION_FAILED`; out-of-scope site refused; root row redacted; `CALL_FAILED` on a site
|
||||
failure; `TIMEOUT` on cancellation. Existing `RunValueChange_*` tests keep passing through the
|
||||
shared helper.
|
||||
- `ScriptTriggerAttributeTests` (Commons): both key spellings, both kinds, case-insensitive type,
|
||||
malformed JSON → false.
|
||||
- `FlowGraphBuilderTests`: a `RouteCall` root with a `ScriptRun` child builds Central → Site.
|
||||
- `RequiredRoleMatrixTests` + `ManagementEndpoints` registration test for the new command.
|
||||
- `FlowDebugServiceTests`: script list from a flattened configuration, write/call classification,
|
||||
prediction shapes, Ask timeout selection.
|
||||
- `FlowDebuggerPageTests` (bUnit): script picker renders options with trigger annotations; Inputs card
|
||||
switches between value field and `ParameterValueForm`; `CanRun` rules; `?script=` deep link.
|
||||
- `FlowDebugCommandsTests` (CLI): `run-script` parsing incl. `--params` JSON errors.
|
||||
- Playwright `FlowDebuggerTests`: Live run of the rig's `OutboundDemo` Call script on `DelmiaSim01`.
|
||||
- `docker/verify-delmia-notifier.sh` step H gains a `run-script` leg against `OutboundDemo`.
|
||||
|
||||
## 9. Documents to update
|
||||
|
||||
`2026-09-05-flow-debugger-design.md` §4 (pointer to this doc), `Component-CentralUI.md` (Flow
|
||||
Debugger entry points), `Component-InboundAPI.md` (FlowDebug commands table),
|
||||
`src/ZB.MOM.WW.ScadaBridge.CLI/README.md`, CLAUDE.md Flow Debugger bullet, and the umbrella index
|
||||
row's Flow Debugger sentence.
|
||||
Reference in New Issue
Block a user