fix(galaxy): AVEVA priority 1 is the most urgent: Galaxy alarm Severity follows AVEVA's priority bands, live and history alike (#887)
v2-ci / unit-tests (push) Has been cancelled

GalaxyAlarmPriority, the one table live Galaxy alarms (MxAccessSeverityMapper)
and AVEVA A&E history (EventMapper, #886) map the AVEVA priority through, ranked
a higher number as more severe. AVEVA ranks priority 1 as the most urgent, so a
priority-1 alarm read 250 and a priority-999 system event 1000.

It now uses AVEVA's default priority ranges, most urgent first:

  AVEVA priority  AVEVA severity  bucket    OPC UA Severity
  1-250           1 Critical      Critical  1000
  251-500         2 High          High       750
  501-750         3 Medium        Medium     500
  751-999         4 Low           Low        250
  (history only)  0 not an alarm  -            1

Out-of-range priorities clamp (<= 0 Critical, > 999 Low). Evidence, vd03
Runtime.dbo.Events read-only: priority 1/100 alarms carry AVEVA severity 1,
400/500 severity 2, 600/700 severity 3, and every priority-999 System.* /
User.* row severity 0 with IsAlarm 0.

- Live and history now use the same number. GalaxyDriver passes the table's
  exact value as AlarmEventArgs.OpcUaSeverity. Before, it passed only the bucket,
  which NativeAlarmProjector re-mapped through its generic 200/500/700/900
  ladder, so live and history disagreed even before the direction fix.
- History: a row whose AVEVA severity is 0 (System.Start/Stop/Deploy,
  User.Write...) is not an alarm and reads Severity 1, below every alarm band.
  For alarms AVEVA's 1-4 severity is still never the value: the live feed
  carries only the priority, so it cannot use it, and history must agree.
- Docs: docs/Historian.md gains "Galaxy alarm priority -> Severity
  (lmxopcua#887)"; docs/AlarmTracking.md updated.
- Live re-run of the #801/#886 harness against vd03 HistorianGateway
  ea9f21c (read-only, network-only): 224 of 233 cases identical to the #886
  run, the 9 others differ only in Severity (priority-700 alarm rows 750 ->
  500, 19 system/user events 1000 -> 1). Record: aveva-history-801.md
  "#887 re-run", report aveva-gate-sdk2-887.json.gz.

Client-visible: every OPC UA client that ranks or filters Galaxy alarms by
Severity sees the order reversed (live and HistoryReadEvents). ScadaBridge has
no code that depends on the old values; its own mixed severity directions are
ScadaBridge#129.

Tests updated because they pinned the inverted table:
- Driver.Galaxy.Tests MxAccessSeverityMapperTests (rewritten: edges 1/250/251/
  500/501/750/751/999, 0/-100/int.MinValue/1000/int.MaxValue clamps,
  monotonic-decreasing sweep, equals GalaxyAlarmPriority for -2..1001)
- Driver.Galaxy.Tests GatewayGalaxyAlarmFeedTests (750 and 600 -> Medium)
- Driver.Historian.Gateway.Tests EventMapperTests (band table, priority 999
  -> 250, 1 -> 1000; system events -> 1)
- OpcUaServer.IntegrationTests GalaxyEquipmentEventHistoryWireTests (700 ->
  500, 100 -> 1000, System.Stop -> 1; the fake now stores AVEVA's real 0-4
  severity)

New tests:
- Runtime.Tests NativeAlarmRestatementEndToEndTests
  .Galaxy_live_condition_severity_is_the_AVEVA_priority_band (8 rows: the real
  feed -> real GalaxyDriver -> host -> projector; the same rows EventMapperTests
  pins on the history path)
- EventMapperTests.Aveva_severity_zero_marks_an_event_that_is_not_an_alarm
  (5 rows, incl. the priority-999 alarm positive control)
- GalaxyDriverAlarmSourceTests: OpcUaSeverity passes through

Mutation check, 8 mutants, all killed: the pre-#887 inverted table; the
driver dropping OpcUaSeverity; no not-an-alarm rule; each of the three band
edges off by one; history ignoring priority; a missing AVEVA severity read
as 0.

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
Joseph Doherty
2026-11-12 12:14:48 -05:00
parent fa811feb3d
commit b61f3e0560
14 changed files with 268 additions and 83 deletions
@@ -108,7 +108,7 @@ each under its own raw device folder.
| Paging | 1-event pages (92 pages) and 7-event pages (14 pages) equal the whole read, row for row. |
| Reverse | Whole and 5-event pages (19 pages): newest first, ties by EventId descending, equal to the expected order. |
| SourceNode / ConditionName | **73 rows** resolve to their live condition: 70 `CPULoadAvg` alarm rows → `Plant/Galaxy/DevWinPlatform/CPULoadHiAlarm` / `CPULoadHiAlarm`, 3 `FRR_Warning` rows → `Plant/Galaxy/Left_001/FRR_Warning` / `FRR_Warning`. Every other row (system and user events) has a null SourceNode. |
| Severity | The wire equals the backend's mapping for every row. Alarm rows (`priority` 500 / 700) read **750**; priority-999 system and user events read **1000** (live Galaxy table; see lmxopcua#887 on its direction). Before: 1 for every row. |
| Severity | The wire equals the backend's mapping for every row. Alarm rows (`priority` 500 / 700) read **750**; priority-999 system and user events read **1000** (live Galaxy table, then still inverted; lmxopcua#887 reversed it, see "#887 re-run" below). Before: 1 for every row. |
`diff_runs.py` against `aveva-gate-sdk2-882.json.gz`: 226 shared cases, **221 identical**. The 5 that differ are expected:
the 4 `DevWinPlatform` event cases (same 72 events, ids, times, sources and messages; only Severity changed from 1 to
@@ -118,6 +118,27 @@ the 4 `DevWinPlatform` event cases (same 72 events, ids, times, sources and mess
Report: [`aveva-history-801/aveva-gate-sdk2-886.json.gz`](aveva-history-801/aveva-gate-sdk2-886.json.gz), sha256 of the
JSON `6ca314ee…c890` (label `sdk2-886-da948e5f-wt`: the #886 working tree on top of `da948e5f`).
## #887 re-run (2026-11-12)
The same harness after lmxopcua#887 reversed the Galaxy priority table (AVEVA priority 1 = most urgent: 1–250 → 1000,
251–500 → 750, 501–750 → 500, 751–999 → 250; AVEVA severity 0, an event that is not an alarm → 1). Same vd03
HistorianGateway `ea9f21c`, same window, read-only, network-only, **no WinRM**. The API key was read over SMB from the
`OtOpcUaHost.reg` export in vd03's `OtOpcUa-backups\pre-20261112-otopcua-5fb4b4db`, kept in a 0600 file, passed by
environment and never printed; the local copies were deleted afterwards.
`diff_runs.py` against `aveva-gate-sdk2-886.json.gz`: 233 cases, **224 identical**, harness failures 0. The 9 that differ
are the 4 `DevWinPlatform` event cases and the 5 `events886.*` wire cases, and **only Severity changed**: same events,
ids, times, sources and messages in every case. On `EQ-886-ZipsenLeft` (92 events):
| Rows | Severity #886 → #887 |
|---|---|
| 70 `DevWinPlatform.CPULoadAvg` alarm rows (priority 500, AVEVA High) | 750 → 750 |
| 3 `Left_001` `FRR_Warning` alarm rows (priority 700, AVEVA Medium) | 750 → **500** |
| 19 system and user events (`System.Start/Stop/Deploy/Undeploy`, `User.Write[.Secured]`; priority 999, AVEVA severity 0) | 1000 → **1** |
Report: [`aveva-history-801/aveva-gate-sdk2-887.json.gz`](aveva-history-801/aveva-gate-sdk2-887.json.gz), sha256 of the
JSON `232f3ee6…5545` (label `sdk2-887-wt`: the #887 working tree on top of `3a312ffd`).
## Files
- [`aveva-history-801/aveva-gate-sdk2-d46044f5.json.gz`](aveva-history-801/aveva-gate-sdk2-d46044f5.json.gz), sha256 of the JSON `00b5ecb6…fc27`.