docs(plans): design for expression-based script & alarm triggers
Captures the brainstormed design for a new Expression trigger: a read-only boolean C# expression evaluated on attribute updates, edge-triggered for scripts and level-based for alarms, compiled against a restricted read-only globals type.
This commit is contained in:
114
docs/plans/2026-05-16-expression-trigger-design.md
Normal file
114
docs/plans/2026-05-16-expression-trigger-design.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Expression Trigger for Template Scripts and Alarms — Design
|
||||
|
||||
**Date:** 2026-05-16
|
||||
**Status:** Approved (brainstorming) — implementation plan to follow
|
||||
|
||||
## Context
|
||||
|
||||
Template scripts and template alarms can only be triggered by single-attribute
|
||||
conditions. Scripts support `Interval`, `ValueChange`, `Conditional`
|
||||
(`{attributeName, operator, threshold}` — one attribute, numeric compare),
|
||||
and `Call`. Alarms support `ValueMatch`, `RangeViolation`, `RateOfChange`, and
|
||||
`HiLo` — all single-attribute. There is no way to trigger on a relationship
|
||||
between *multiple* attributes (e.g. "speed is high *and* mode is Run").
|
||||
|
||||
This design adds an **Expression trigger**: a user-supplied read-only boolean
|
||||
C# expression, evaluated whenever an instance attribute updates, that fires the
|
||||
script / activates the alarm when it returns true. It generalizes the existing
|
||||
single-attribute `Conditional` trigger.
|
||||
|
||||
### Decisions taken during brainstorming
|
||||
|
||||
- The trigger is a **read-only boolean expression** — no `External`/`Database`/
|
||||
`Notify`/`CallScript` side effects. It must be cheap and safe to run on every
|
||||
attribute update.
|
||||
- **Scripts fire edge-triggered** — once per `false→true` transition.
|
||||
- **Alarms are level-based** — active while the expression is true, clear when
|
||||
false (consistent with all existing alarm trigger types).
|
||||
- **Evaluation approach B** — compile against a *restricted read-only globals
|
||||
type*, so read-only is enforced, not merely conventional. Reuses the existing
|
||||
Roslyn compilation pipeline.
|
||||
|
||||
## Design
|
||||
|
||||
### 1. Trigger model & storage
|
||||
|
||||
- **Scripts:** `TemplateScript.TriggerType` (`string?`) gains the value
|
||||
`"Expression"`. `TriggerConfiguration` JSON is `{ "expression": "<C#>" }`.
|
||||
- **Alarms:** `AlarmTriggerType` enum gains a member `Expression`.
|
||||
`TriggerConfiguration` JSON is the same `{ "expression": "<C#>" }`.
|
||||
- The expression is a bare C# boolean expression (no `return` keyword — Roslyn
|
||||
scripting returns the trailing expression's value), e.g.
|
||||
`Attributes["Speed"] > 1000 && (string)Attributes["Mode"] == "Run"`.
|
||||
- Entity types unchanged: both `TriggerConfiguration` fields stay `string?`.
|
||||
|
||||
Adding the `AlarmTriggerType` member touches three switch sites:
|
||||
`AlarmActor.ParseEvalConfig`, `AlarmActor.HandleAttributeValueChanged`,
|
||||
`AlarmTriggerConfigCodec`.
|
||||
|
||||
### 2. Runtime evaluation
|
||||
|
||||
- **`TriggerExpressionGlobals`** (new, `ScadaLink.SiteRuntime`) — a read-only
|
||||
globals type exposing only `Attributes["X"]`, `Children["C"].Attributes["X"]`,
|
||||
and `Parent.Attributes["X"]`, backed by an in-memory snapshot dictionary. No
|
||||
side-effecting APIs. A missing attribute reads as `null` (never throws).
|
||||
- The expression is compiled once via the existing Roslyn pipeline (same
|
||||
forbidden-API trust checks) against `TriggerExpressionGlobals`; the compiled
|
||||
delegate is cached on the actor.
|
||||
- **Attribute snapshot:** `ScriptActor` and `AlarmActor` already receive every
|
||||
`AttributeValueChanged`. Each keeps a local `Dictionary<string,object?>`
|
||||
snapshot — seeded from the instance's initial attribute set at startup, then
|
||||
updated on each change. The expression evaluates against the snapshot — no
|
||||
`Ask` back to the `InstanceActor`; cheap and re-entrancy-free.
|
||||
- **On each `AttributeValueChanged`:** update snapshot → run cached expression
|
||||
→ `bool`.
|
||||
- **Script (edge):** track the previous result; on `false→true`, run the
|
||||
script (spawn `ScriptExecutionActor`, as the other triggers do).
|
||||
- **Alarm (level):** the `bool` feeds the existing binary Normal↔Active state
|
||||
machine — raise on `→Active`, clear on `→Normal`.
|
||||
- Cost per attribute update: one cached-delegate call + one bool compare.
|
||||
|
||||
### 3. Editors & analysis
|
||||
|
||||
- **`ScriptTriggerEditor`:** add `Expression` to `ScriptTriggerKind` and
|
||||
`ScriptTriggerConfigCodec` (round-trips `{ expression }`).
|
||||
- **`AlarmTriggerEditor`:** add an `Expression` case to its trigger `@switch`.
|
||||
- Both render the same **expression panel**: a compact `MonacoEditor`
|
||||
(~120 px) with C# syntax, `Attributes["..."]` completion driven by the
|
||||
template's attribute metadata (self / children / parent), and live compile
|
||||
diagnostics. A one-line hint summarizes what fires.
|
||||
- **Analysis:** reuse the existing `Template` analysis kind — completion and
|
||||
diagnostics work with no new analyzer code. Editor completion is slightly
|
||||
permissive (also shows `Instance`/`External`), but the runtime's restricted
|
||||
`TriggerExpressionGlobals` is what enforces read-only. A dedicated strict
|
||||
analysis kind is a possible later refinement, out of scope here.
|
||||
|
||||
### 4. Error handling & validation
|
||||
|
||||
- **Pre-deployment:** extend `ValidationService` to compile-check expression
|
||||
triggers (against `TriggerExpressionGlobals`); compile errors block
|
||||
deployment and surface like other validation errors. Unknown
|
||||
`Attributes["..."]` keys are flagged as the existing trigger-reference
|
||||
validation does.
|
||||
- **Runtime — expression throws:** caught; treated as `false` for that update;
|
||||
a script-error event is written to the site event log. The actor never
|
||||
crashes.
|
||||
- **Non-bool result:** treated as `false` and logged.
|
||||
- **Missing attribute:** reads as `null` (handled in `TriggerExpressionGlobals`).
|
||||
- **Blank expression:** the trigger is inert; validation emits a warning.
|
||||
|
||||
### 5. Testing & verification
|
||||
|
||||
- **Unit:** codec round-trip for script and alarm `{ expression }`; expression
|
||||
compile (valid + invalid).
|
||||
- **Runtime:** deploy an instance with an expression-triggered script and
|
||||
alarm; drive attribute updates (bound Test Run / CLI); confirm the script
|
||||
fires only on `false→true` and the alarm raises/clears with the expression.
|
||||
- **UI:** the expression panel in both editors; save → reopen round-trip.
|
||||
|
||||
## Implementation tasks
|
||||
|
||||
- #25 — Implement expression trigger model + codecs
|
||||
- #26 — Implement runtime expression evaluation (blocked by #25)
|
||||
- #27 — Add expression panel to the trigger editors (blocked by #25)
|
||||
- #28 — Validate expression triggers pre-deployment (blocked by #25, #26)
|
||||
Reference in New Issue
Block a user