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