From c94d3b75702310e6e7d1d6cf5659d03ba4fa9663 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 16 May 2026 05:21:57 -0400 Subject: [PATCH] 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. --- .../2026-05-16-expression-trigger-design.md | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 docs/plans/2026-05-16-expression-trigger-design.md diff --git a/docs/plans/2026-05-16-expression-trigger-design.md b/docs/plans/2026-05-16-expression-trigger-design.md new file mode 100644 index 0000000..5f48207 --- /dev/null +++ b/docs/plans/2026-05-16-expression-trigger-design.md @@ -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": "" }`. +- **Alarms:** `AlarmTriggerType` enum gains a member `Expression`. + `TriggerConfiguration` JSON is the same `{ "expression": "" }`. +- 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` + 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)