feat(alarms): scripted condition Quality from worst-of-input tag quality (#478) #480

Merged
dohertj2 merged 2 commits from feat/scripted-alarm-quality-478 into master 2026-07-17 16:08:09 -04:00
Owner

Summary

Layer 3 of #477 — a scripted OPC UA alarm condition's Quality now reflects the worst quality across its input tags, mirroring the native OT semantic ("can I trust this condition's state?"). Previously ScriptedAlarmHostActor.ToSnapshot hardcoded Good.

Closes #478.

Two blockers found on the live path (both silently discarded quality)

  1. DependencyMuxActor built DependencyValueChanged without the AttributeValuePublished.Quality it already carried.
  2. ScriptedAlarmHostActor.OnDependencyChanged pushed each mux value into the engine with a hardcoded 0u (Good) StatusCode.

So even a Bad driver value reached the engine as Good — Layer 3 plumbs quality first.

Design

  • Plumb quality end-to-end. DependencyValueChanged gains Quality (defaulted Good, VT engine unchanged); the mux forwards it; OnDependencyChanged maps it to a StatusCode on the pushed snapshot.
  • Engine computes the worst input quality each evaluation and carries it on ScriptedAlarmEvent.WorstInputStatusCode. A real transition carries it → ToSnapshot projects it (no clobber-back-to-Good when a transition fires while an input is Uncertain).
  • Bad input freezes the condition (AreInputsReady holds state — no transition), exactly like a comms-lost native driver. A quality-bucket change with no transition emits the new EmissionKind.QualityChanged, routed to the existing #477-L2 AlarmQualityUpdate → WriteAlarmQuality node path (quality only, one Part 9 event per bucket change, no /alerts row, no historian write). No new sink surface.
  • ScriptedAlarmSource skips QualityChanged so it never fabricates a phantom IAlarmSource event (which would materialize/historize a condition).
  • Cold start is not a quality signal. An input not yet published contributes Good (the AreInputsReady guard already holds the condition), so scripted conditions don't flash Bad at deploy; the first actually-Bad published value flips the bucket and annotates.

Tests (TDD, RED-first)

  • Engine: transition carries Uncertain; Bad input with no transition emits QualityChanged(Bad); restore emits QualityChanged(Good); unchanged bucket emits nothing.
  • ScriptedAlarmSource: QualityChanged raises no OnAlarmEvent.
  • Mux: DependencyValueChanged carries the published quality.
  • Host: Bad dependency → AlarmQualityUpdate(Bad) + no /alerts; transition snapshot carries the worst quality.

All green: Core.ScriptedAlarms 76/76, Runtime 386/386; full solution builds clean. Docs: AlarmTracking.md Layer-3 section + design doc.

## Summary **Layer 3 of #477** — a scripted OPC UA alarm condition's `Quality` now reflects the **worst** quality across its input tags, mirroring the native OT semantic ("can I trust this condition's state?"). Previously `ScriptedAlarmHostActor.ToSnapshot` hardcoded `Good`. Closes #478. ## Two blockers found on the live path (both silently discarded quality) 1. `DependencyMuxActor` built `DependencyValueChanged` **without** the `AttributeValuePublished.Quality` it already carried. 2. `ScriptedAlarmHostActor.OnDependencyChanged` pushed each mux value into the engine with a **hardcoded `0u` (Good)** StatusCode. So even a `Bad` driver value reached the engine as Good — Layer 3 plumbs quality first. ## Design - **Plumb quality end-to-end.** `DependencyValueChanged` gains `Quality` (defaulted `Good`, VT engine unchanged); the mux forwards it; `OnDependencyChanged` maps it to a StatusCode on the pushed snapshot. - **Engine computes the worst input quality** each evaluation and carries it on `ScriptedAlarmEvent.WorstInputStatusCode`. A **real transition** carries it → `ToSnapshot` projects it (no clobber-back-to-Good when a transition fires while an input is `Uncertain`). - **Bad input freezes the condition** (`AreInputsReady` holds state — no transition), exactly like a comms-lost native driver. A quality-bucket change with no transition emits the new `EmissionKind.QualityChanged`, routed to the **existing** #477-L2 `AlarmQualityUpdate → WriteAlarmQuality` node path (quality only, one Part 9 event per bucket change, **no `/alerts` row**, no historian write). No new sink surface. - **`ScriptedAlarmSource` skips `QualityChanged`** so it never fabricates a phantom `IAlarmSource` event (which would materialize/historize a condition). - **Cold start is not a quality signal.** An input not yet published contributes `Good` (the `AreInputsReady` guard already holds the condition), so scripted conditions don't flash `Bad` at deploy; the first actually-`Bad` published value flips the bucket and annotates. ## Tests (TDD, RED-first) - **Engine:** transition carries `Uncertain`; `Bad` input with no transition emits `QualityChanged(Bad)`; restore emits `QualityChanged(Good)`; unchanged bucket emits nothing. - **`ScriptedAlarmSource`:** `QualityChanged` raises no `OnAlarmEvent`. - **Mux:** `DependencyValueChanged` carries the published quality. - **Host:** `Bad` dependency → `AlarmQualityUpdate(Bad)` + no `/alerts`; transition snapshot carries the worst quality. All green: Core.ScriptedAlarms 76/76, Runtime 386/386; full solution builds clean. Docs: `AlarmTracking.md` Layer-3 section + design doc.
dohertj2 added 1 commit 2026-07-17 15:57:07 -04:00
feat(alarms): scripted condition Quality from worst-of-input tag quality (#478)
v2-ci / build (pull_request) Successful in 3m48s
v2-ci / unit-tests (pull_request) Failing after 11m0s
8c5e2be92e
Layer 3 of #477: a scripted alarm's condition Quality now reflects the WORST
quality across its input tags, mirroring the native OT semantic (#477 L2).

Plumbing (quality was silently discarded twice on the live path):
- VirtualTagActor.DependencyValueChanged gains Quality (defaulted Good); the
  DependencyMuxActor forwards the published AttributeValuePublished.Quality it
  already carried; ScriptedAlarmHostActor.OnDependencyChanged pushes the real
  quality into the engine (was hardcoded 0u/Good).

Engine (Core.ScriptedAlarms):
- ScriptedAlarmEngine computes worst-of-input quality each eval (skipping
  not-yet-published inputs, which are a readiness concern, not a quality signal)
  and carries it on ScriptedAlarmEvent.WorstInputStatusCode.
- A real transition carries the current worst quality so ToSnapshot's full
  snapshot doesn't clobber quality back to Good (e.g. transition while Uncertain).
- A Bad input freezes the condition (no transition), like a comms-lost native
  driver; a quality-bucket change with no transition emits the new
  EmissionKind.QualityChanged, routed to the existing #477-L2
  AlarmQualityUpdate -> WriteAlarmQuality node path (quality only, no /alerts
  row, no historian write). ScriptedAlarmSource skips QualityChanged so it never
  fabricates a phantom IAlarmSource event.

Host: ToSnapshot maps WorstInputStatusCode -> OpcUaQuality; OnEngineEmission
routes QualityChanged out of band.

Tests (TDD, RED-first): engine worst-carry + Bad/restore QualityChanged +
unchanged-bucket-no-emit; source swallows QualityChanged; mux forwards quality;
host Bad-dep -> AlarmQualityUpdate(no alerts) + transition snapshot carries worst.
Docs: AlarmTracking.md Layer-3 section + design doc.

Closes #478
dohertj2 added 1 commit 2026-07-17 16:07:59 -04:00
docs(alarms): state #478 coverage boundary + file Layer-4 comms-loss follow-up (#481)
v2-ci / build (pull_request) Successful in 5m42s
v2-ci / unit-tests (pull_request) Failing after 13m11s
043e237dba
Post-implementation review (HIGH finding) noted #478's mux-delivered
input-quality path does not cover a driver comms-loss: a poll driver
(Modbus/S7) whose device goes unreachable emits only ConnectivityChanged and
goes silent on the value feed, so a scripted alarm keeps the last Good value.
The code as shipped faithfully implements #478's written scope (worst of input
tags' qualities via the dependency mux). The comms-loss bridge for scripted
alarms (symmetric of native #477-L2, plus the null-value/cold-start asymmetry
and its VT-quality ripple) is tracked as #481. Docs updated in
AlarmTracking.md + the design doc.
dohertj2 merged commit 2cae4c8f01 into master 2026-07-17 16:08:09 -04:00
dohertj2 deleted branch feat/scripted-alarm-quality-478 2026-07-17 16:08:09 -04:00
Sign in to join this conversation.