# MES Alarm-Status API — Implementation Plan **Date:** 2026-06-30 **Status:** **Phase 1 [repo] COMPLETE 2026-08-01** — the `Alarms.CurrentAsync()` script accessor and the `AckTime` native-mirror enrichment are shipped (see §7). Phases 2–4 are **deployed config** (inbound methods + CvdReactor template scripts) and still pending; they need a live rig. All §6 open questions DECIDED 2026-08-01 (design review with user). **Component touchpoints:** Inbound API (#14), Script Analysis (#25), Site Runtime (#3), Template Engine (#1) — plus deployed config (inbound methods + CvdReactor template scripts). --- ## 1. Goal Port the legacy **WWSupport / APIServer** MES alarm-status endpoints onto ScadaBridge's Inbound API, mirroring how `MesMoveIn` / `MesMoveOut` were ported: 1. **Update** the existing `SimpleAlarmStatusRequest` inbound method (currently a stub) to do real work. 2. **Create** a new `AlarmStatus` inbound method (the full, filtered endpoint). 3. Both inbound methods **forward to site scripts** (`SAPID → BTDB machine lookup → Route.To(code).Call(...)`), exactly like `MesMoveIn`. 4. ~~The generic version on the `MESReceiver` template queries the BTDB `MachineAlarm` table.~~ **DROPPED (Q2, 2026-08-01):** no MESReceiver version ships — a config-only answer with no live state was judged not worth having. Machines whose template doesn't implement the scripts get a clean `WasSuccessful=false` "not supported on this machine" error from the inbound router. 5. The **`CvdReactor` template provides the (only) implementation**, reading the **alarms actually defined on the CvdReactor object** (its 7 native alarm-source bindings) directly — no `MachineAlarm` lookup anywhere. Legacy spec being mirrored: [`docs/former-api-specs/mes/Alarm-API.md`](../former-api-specs/mes/Alarm-API.md). --- ## 2. Legacy contract recap (target parity) Two endpoints, **one** response shape (`AlarmStatusResponse`): | Legacy route | New inbound method | Request | |---|---|---| | `POST /mes/simplealarmstatus` | `SimpleAlarmStatusRequest` (exists, id 9) | `{ "SAPID": "..." }` | | `POST /mes/alarmstatus` | `AlarmStatus` (NEW) | `{ MachineFilter, AlarmFilter }` | **`AlarmStatusResponse`** = `{ WasSuccessful: bool, ErrorText: string, Alarms: AlarmInfo[] }`; on failure `WasSuccessful=false` and `Alarms` is **cleared** (all-or-nothing). **`AlarmInfo`** = `Name`, `HierarchicalName` (`{Code}.{Name}`), `Description`, `IsFlaggedForMES`, `Severity` (0–999), `StatusCode` (`"Triggered"` / `"Triggered.Acked"`), `TriggeredDT`, `AckDT?`, `AckComment`. **Behaviors to preserve:** - Only **triggered** alarms returned (`InAlarm == true`). No "all configured" mode. `IncludeTriggered` is a no-op (legacy declared-but-unused). - `/simplealarmstatus`: always **flagged-only**, ack filtering ignored (acked alarms always included, tagged `Triggered.Acked`). - `/alarmstatus`: `MachineFilter` precedence `SAPID → Code → ZTag → MachineID`; `AlarmFilter` order `FlaggedOnly → MinSeverity → MaxSeverity → NameFilter` (substring, case-insensitive; severity bounds inclusive); `IncludeAcked=false` excludes acked. --- ## 3. Current state in ScadaBridge (verified 2026-06-30 against wonder-app-vd03) **Inbound methods (`api-method list`):** - `SimpleAlarmStatusRequest` (id 9) — **stub**: `return new { WasSuccessful = true, Alarms = Array.Empty() };`. Param: `SAPID` (string). **Return definition already shaped to `AlarmInfo`** (Name/HierarchicalName/Description/IsFlaggedForMES/Severity/StatusCode/TriggeredDT/AckDT/AckComment). ✅ no return-contract change needed. - No `AlarmStatus` method yet. - Reference pattern (`MesMoveIn`, id 2): `SAPID → Database.QuerySingleAsync("BTDB", "SELECT TOP 1 Code FROM dbo.Machine WHERE SAPID=@s") → Route.To(code).Call("MesMoveIn", {...})`. **Templates (`template list` / `template get`):** - `CvdReactor` (id 1, parent=none): root scripts `IpsenMoveIn`, `ProcessRecipeDownload`, `MesMoveIn`, `MesMoveOut`; **7 native alarm sources** (all over `MxGateway`): `ReactorAlarms` (`Z28061.`), `LeftSideAlarms` (`Left_002.`), `RightSideAlarms` (`Right_002.`), `LeftLeakTestAlarms` (`LeakTest_003.`), `RightLeakTestAlarms` (`LeakTest_004.`), `LeftTableAlarms` (`TableAlarms_003.`), `RightTableAlarms` (`TableAlarms_004.`); composes templates 5,6,8,9,13,14,26. No computed `TemplateAlarm`s. - `MESReceiver` (id 3, parent=none): root scripts `MoveIn`, `MoveOut` (scope-bound to the composed receiver, bare `Attributes`); `Left`/`RightMESReceiver` (5/6) derive from it and are composed into CvdReactor. No alarms, no native sources. **Established override pattern (MoveIn):** `CvdReactor.MesMoveIn` (root) does side-dispatch off the SAPID suffix and writes **directly** into `Children["LeftMESReceiver"|"RightMESReceiver"].Attributes` via `WriteBatchAndWaitAsync` — it does **not** `CallScript` the base. `MESReceiver.MoveIn` is the generic scope-bound implementation. So "override" here = **CvdReactor supplies its own root-level script that `Route.Call` resolves for CvdReactor instances**; the MESReceiver version is the reusable/generic one. (CvdReactor does **not inherit** MESReceiver — it composes it — so this is parallel-definition, not inheritance-override.) --- ## 4. The blocking gap (drives most of the code work) **Site `call`-type scripts currently have NO public API to read alarm condition state.** From the site-script API map: - The runtime context is `ScriptRuntimeContext` (`src/.../SiteRuntime/Scripts/ScriptRuntimeContext.cs`); compile-time mirror is `ScriptCompileSurface` (`src/.../ScriptAnalysis/ScriptCompileSurface.cs`). Members today: `Attributes`, `Children`, `Parent`, `Instance` (GetAttribute/SetAttribute/CallScript), `Database`, `ExternalSystem`, `Notify`, `Scripts`, `Tracking`, `Parameters`, `Alarm` (on-trigger only), `CancellationToken`. - Computed alarm state is only exposed via the `Alarm` global **passed into on-trigger scripts**. Native alarm state (`AlarmConditionState`) lives in the Instance Actor's `_latestAlarmEvents`; **no script-facing query method exists**. ⇒ The CvdReactor override ("read the defined alarms directly") **cannot be written as a script today**. It requires a new enabling capability: a script-facing **`Alarms` read accessor**. **Good news — the data already exists and is local.** `DebugViewSnapshot` (`Commons/Messages/DebugView/DebugViewSnapshot.cs`) carries `IReadOnlyList AlarmStates`, and the M7 Alarm Summary page already fans this out per instance. `AlarmStateChanged` (`Commons/Messages/Streaming/AlarmStateChanged.cs`) carries everything we need: `AlarmName`, `Condition` (`AlarmConditionState`: Active/Acknowledged/Confirmed/Shelve/Suppressed/Severity), `Priority`, `Level`, `Message`, `Kind`, `SourceReference`, `AlarmTypeName`, `Category`, `OperatorUser`, `OperatorComment`, `OriginalRaiseTime`, `Timestamp`, `NativeSourceCanonicalName`, `IsConfiguredPlaceholder`. The script runs **inside the Instance Actor's context**, so an `Alarms` accessor reads this via a **local Ask** — same mechanism as `GetAttribute`, no cross-cluster hop. --- ## 5. Target architecture (three layers) ``` INBOUND (central) ENABLING CODE (repo) SITE TEMPLATE SCRIPTS (deployed config) ───────────────── ──────────────────── ─────────────────────────────────────── SimpleAlarmStatusRequest ─┐ Alarms script accessor ├─ Route.To(code).Call(...) ─────────► CvdReactor.SimpleAlarmStatus (native sources) AlarmStatus (new) ─┘ + AckTime mirror enrichment ──► CvdReactor.AlarmStatus (native sources) (ScriptRuntimeContext + ScriptCompileSurface) (no MESReceiver version — Q2 DECIDED: dropped) ``` ### 5.1 Layer A — Inbound API methods (deployed config; central) Thin routers, identical shape to `MesMoveIn`. They own machine resolution (the `MachineFilter` precedence) against BTDB, then `Route.To(code).Call(, )` and return the site result verbatim. **`SimpleAlarmStatusRequest` (update id 9):** extract the **numeric** SAP id for the BTDB lookup, but forward the **raw** SAPID (with `_A`/`_B`[`_LT`] suffix) to the site so the CvdReactor override can resolve the side — exactly as `MesMoveIn` does. ```csharp try { var raw = ((Parameters.ContainsKey("SAPID") ? Parameters["SAPID"]?.ToString() : null) ?? "").Trim(); var m = System.Text.RegularExpressions.Regex.Match(raw, @"^(\d+)"); if (!m.Success) return new { WasSuccessful = false, ErrorText = $"SAPID does not contain a numeric SAP id: '{raw}'", Alarms = Array.Empty() }; var sap = m.Groups[1].Value; // numeric SAP for the DB lookup var code = await Database.QuerySingleAsync("BTDB", "SELECT TOP 1 Code FROM dbo.Machine WHERE SAPID=@s", new { s = sap }); if (string.IsNullOrEmpty(code)) return new { WasSuccessful = false, ErrorText = $"Failed to find machine with SAPID '{sap}'", Alarms = Array.Empty() }; // SimpleAlarmStatus semantics: flagged-only, acked always included. Pass RAW SAPID for side routing. return await Route.To(code).Call("SimpleAlarmStatus", new { SAPID = raw, MachineCode = code }); } catch (Exception ex) { return new { WasSuccessful = false, ErrorText = "SimpleAlarmStatus failed: " + ex.Message, Alarms = Array.Empty() }; } ``` **`AlarmStatus` (new method):** nested `MachineFilter` + `AlarmFilter` params (Inbound API extended type system supports Object). Resolve machine via precedence (all selectors come from `dbo.Machine`), then forward the filter to the site script. - Param definition (object): `MachineFilter { MachineID:int?, SAPID:string, ZTag:string, Code:string }`, `AlarmFilter { NameFilter:string, MinSeverity:int?, MaxSeverity:int?, IncludeTriggered:bool, IncludeAcked:bool, FlaggedOnly:bool }`. - Return definition: same `AlarmInfo[]` shape as id 9. - Body: resolve `code` by `SAPID → Code → ZTag → MachineID` (each its own `SELECT TOP 1 Code FROM dbo.Machine WHERE ...`), selector-specific error text on unmatched, then `Route.To(code).Call("AlarmStatus", new { MachineCode = code, SAPID = , ...AlarmFilter fields... })`. The raw `MachineFilter.SAPID` (suffix included) is forwarded so the CvdReactor override can scope by side; when the machine was selected by `Code`/`ZTag`/`MachineID` (no SAPID suffix), side scoping is skipped and all sources are returned (see §6.6). > **Note:** Machine resolution stays on the inbound side (central) because it is environment/SQL-shaped and identical for both endpoints; site scripts receive an already-resolved `MachineCode` plus the alarm filter. > **Unsupported machines (Q2 DECIDED):** since only CvdReactor implements the site scripts, `Route.Call` on any other machine fails script-not-found. Both routers catch that case specifically and return `{ WasSuccessful=false, ErrorText = $"Alarm status is not supported on machine '{code}'" }` instead of the raw exception text (verify during impl what exception/message `Route.Call` yields for a missing script so the catch can distinguish it). ### 5.2 Layer B — Enabling code change: script-facing `Alarms` accessor (repo) New read-only accessor on the site script surface so scripts can enumerate the instance's current alarms. **Proposed shape** (final names TBD in review): ```csharp // On ScriptRuntimeContext (runtime) + ScriptCompileSurface (compile-time stub): public AlarmsAccessor Alarms { get; } public sealed class AlarmsAccessor { // Snapshot of the instance's CURRENT alarm conditions (computed + native), // read locally from the Instance Actor (same Ask path as attribute reads). Task> CurrentAsync(CancellationToken ct = default); } public sealed record ScriptAlarm( string Name, string SourceReference, string NativeSourceCanonicalName, bool Active, bool Acknowledged, bool? Confirmed, bool Shelved, bool Suppressed, int Severity, string Kind, // "Computed" | "NativeOpcUa" | "NativeMxAccess" string Message, string AlarmTypeName, string Category, string OperatorUser, string OperatorComment, DateTimeOffset? OriginalRaiseTime, DateTimeOffset Timestamp, DateTimeOffset? AckTime, // Q4 DECIDED: real ack timestamp from the enriched mirror string CurrentValue, string LimitValue, bool IsConfiguredPlaceholder); ``` **Ack-timestamp enrichment (Q4 DECIDED 2026-08-01 — enrich the mirror now, not a follow-up):** - Add an additive `AckTime` (`DateTimeOffset?`) to `AlarmStateChanged`, the vendored `AlarmStateUpdate` proto (additive field number, never reuse — manual toggle-build-copy-untoggle regen), and the site `native_alarm_state` persistence so it survives failover. - Semantics: when the underlying source supplies a true ack time (OPC UA A&C ack transitions do), use it; when it doesn't (MxGateway events without one), stamp the observation time of the ack transition at the DCL — accurate to when the system saw the ack, never fabricated. Null while unacked; cleared on re-raise. Implementation: - Runtime: `Alarms.CurrentAsync()` Asks the Instance Actor for its alarm snapshot (reuse the existing internal alarm-state map that feeds `DebugViewSnapshot.AlarmStates`; project each `AlarmStateChanged` → `ScriptAlarm`). New internal request/response message (or reuse `DebugSnapshotRequest` and project off `AlarmStates`). - Compile surface: matching stub returning `Task.FromResult(empty)` so design-time compile + Test Run pass. - `ScriptTrustPolicy` (#25): no new forbidden surface; `Alarms` is an allow-listed context member like `Attributes`. Confirm the validator's allow-list includes it. - Additive message-contract evolution only. ### 5.3 Layer C — Site template scripts (deployed config) **MESReceiver version — DROPPED (Q2 DECIDED 2026-08-01).** With no native alarm sources, MESReceiver has no live "triggered" signal; a configured-catalog-only answer was judged misleading rather than useful. No BTDB `MachineAlarm` read ships anywhere in this feature. Machines without the CvdReactor-style scripts get the router's "not supported on machine" error (§5.1). **CvdReactor (the only implementation — reads native sources directly, no DB):** `SimpleAlarmStatus` and `AlarmStatus` (Q5 DECIDED: names match the endpoints, template-agnostic contract) as **root-level scripts** (mirrors `MesMoveIn`). The override **routes left vs right off the SAPID suffix** — `_A` ⇒ Left, `_B` ⇒ Right — and scopes the returned alarms to that side's native sources plus the shared reactor-wide source. The `_LT` (leak-test) suffix is **ignored** for scoping: it is stripped before reading the side, and both the side's run and leak-test sources are included. Source → side map (the 7 `CvdReactor` native sources): | Side (`_A`/`_B`) | Native sources included | |---|---| | `Left` (`_A`) | `LeftSideAlarms`, `LeftLeakTestAlarms`, `LeftTableAlarms`, **+ shared `ReactorAlarms`** | | `Right` (`_B`) | `RightSideAlarms`, `RightLeakTestAlarms`, `RightTableAlarms`, **+ shared `ReactorAlarms`** | ```csharp // CvdReactor.SimpleAlarmStatus (sketch) // Q1 DECIDED: MES relevance = dedicated severity band. Galaxy alarm priorities for // MES-relevant alarms are configured into 900-999; nothing else may use that band. const int MesBandMin = 900; const int MesBandMax = 999; try { var raw = (Parameters["SAPID"] as string) ?? ""; var code = Parameters["MachineCode"]?.ToString() ?? ""; // Side routing from the SAPID suffix: _A => Left, _B => Right. _LT (leak test) is ignored. var core = raw.EndsWith("_LT") ? raw.Substring(0, raw.Length - 3) : raw; var side = core.EndsWith("_A") ? "Left" : core.EndsWith("_B") ? "Right" : null; if (side == null) return new { WasSuccessful = false, ErrorText = "SAPID missing _A/_B side suffix", Alarms = Array.Empty() }; // Keep this side's sources (Left*/Right*) + the shared reactor-wide source; drop the other side's. System.Func InScope = src => string.IsNullOrEmpty(src) // computed/unbound — keep || src.StartsWith(side, System.StringComparison.OrdinalIgnoreCase) // Left* or Right* || src.StartsWith("Reactor", System.StringComparison.OrdinalIgnoreCase); // shared ReactorAlarms var alarms = await Alarms.CurrentAsync(); // native + computed, all 7 sources var infos = alarms .Where(a => a.Active && !a.IsConfiguredPlaceholder) // only triggered .Where(a => InScope(a.NativeSourceCanonicalName)) // _A => Left*, _B => Right*, + ReactorAlarms // SimpleAlarmStatus semantics: flagged-only (= MES band, Q1) + acked always included .Where(a => a.Severity >= MesBandMin && a.Severity <= MesBandMax) .Select(a => new { Name = a.Name, HierarchicalName = code + "." + a.Name, Description = string.IsNullOrEmpty(a.Message) ? a.AlarmTypeName : a.Message, // Q3 DECIDED IsFlaggedForMES = a.Severity >= MesBandMin && a.Severity <= MesBandMax, // Q1 DECIDED: real predicate Severity = a.Severity, StatusCode = a.Acknowledged ? "Triggered.Acked" : "Triggered", TriggeredDT = (a.OriginalRaiseTime ?? a.Timestamp), AckDT = a.AckTime, // Q4 DECIDED: enriched mirror AckComment = a.OperatorComment, }).ToList(); return new { WasSuccessful = true, ErrorText = (string)null, Alarms = infos }; } catch (Exception ex) { return new { WasSuccessful = false, ErrorText = "SimpleAlarmStatus failed: " + ex.Message, Alarms = Array.Empty() }; } ``` `CvdReactor.AlarmStatus` is the same — same side parsing + `InScope` filter, WITHOUT the always-on band filter — but applies the passed `AlarmFilter` (NameFilter/MinSeverity/MaxSeverity/FlaggedOnly/IncludeAcked) over the scoped native list; `FlaggedOnly=true` means the MES-band predicate (Q1). `IsFlaggedForMES` is always reported per-row from the band predicate. (When `AlarmStatus` was selected by `Code`/`ZTag`/`MachineID` with no SAPID suffix, `side == null`; in that case return all sources instead of erroring — Q6b DECIDED.) > **Shared `ReactorAlarms` (`Z28061.`) is included on both sides — Q6a DECIDED 2026-08-01:** reactor-wide faults apply regardless of which side MES asks about. --- ## 6. Key design decisions — ALL DECIDED 2026-08-01 (design review with user) 1. **`IsFlaggedForMES` for native alarms — DECIDED: severity band.** MES relevance is encoded in the alarm severity itself: MES-relevant alarms are configured (in the Galaxy alarm priority) into a **dedicated band 900–999** reserved exclusively for MES-relevant alarms, so it cannot collide with ordinary criticality tuning. Scripts carry `MesBandMin = 900` / `MesBandMax = 999` as named constants; `IsFlaggedForMES = (Severity in band)` is reported honestly per row; `SimpleAlarmStatus` (always flagged-only) and `AlarmStatus` with `FlaggedOnly=true` filter by the band predicate. Since `Severity` is returned verbatim, MES's own `MinSeverity`/`MaxSeverity` filters compose naturally with the band. No new tables, code flags, or allow-lists. **Operational prerequisite:** the Galaxy alarm priorities for MES-relevant CvdReactor alarms must be set into 900–999 before the endpoints are meaningful. 2. **MESReceiver version — DECIDED: dropped entirely.** With no native alarm sources there is no live state; a config-only catalog answer was judged not worth shipping. Only CvdReactor implements the scripts; other machines get the router's clean "not supported on machine" error (§5.1). No BTDB `MachineAlarm` dependency remains. 3. **`Description` mapping — DECIDED:** `Message`, falling back to `AlarmTypeName` when `Message` is empty. 4. **`AckDT` mapping — DECIDED: enrich the native mirror now.** Additive `AckTime` on `AlarmStateChanged` + vendored proto + `native_alarm_state` (survives failover). True source ack time where available (OPC UA A&C); DCL observation time of the ack transition where the source lacks one (MxGateway); null while unacked; cleared on re-raise. See §5.2. 5. **Script naming — DECIDED: `SimpleAlarmStatus` / `AlarmStatus`** (match the endpoint names; template-agnostic contract any future machine template can implement). With the MESReceiver version dropped there is no parallel definition, so no override-resolution concern remains. 6. **Side scoping — DECIDED (all parts).** CvdReactor-only: `_A` ⇒ Left, `_B` ⇒ Right; `_LT` stripped before reading the side (leak-test sources still included). (a) shared `ReactorAlarms` (`Z28061.`) **included on both sides** — reactor-wide faults apply regardless of side. (b) missing suffix: `SimpleAlarmStatus` **errors** on a SAPID without `_A`/`_B` (matches `MesMoveIn`); `AlarmStatus` selected by `Code`/`ZTag`/`MachineID` (no SAPID) **returns all sources**; a SAPID selector without a suffix errors. 7. **Roles / auth — DECIDED:** no new roles; create `AlarmStatus` as Designer via CLI; authorize the **existing MES API key** (the one already calling `MesMoveIn`/`MesMoveOut`) for both alarm endpoints — one key per external system. --- ## 7. Implementation task breakdown (ordered) > Two artifact classes: **[repo]** = source/tests/docs committed to git; **[deployed]** = inbound methods / template scripts pushed to the cluster via CLI/UI (not in repo). The user said "don't execute yet" — this is the ordered plan only. **Phase 1 — Enabling `Alarms` script API + AckTime mirror enrichment [repo]** — ✅ **DONE 2026-08-01** 1. ✅ **DONE 2026-08-01** — **AckTime enrichment (§6.4):** additive `AckTime` on `AlarmStateChanged`, the vendored `AlarmStateUpdate` proto (manual regen), `native_alarm_state` persistence, and the DCL ack-transition stamping (source ack time where supplied, else observation time). - `AlarmStateChanged.AckTime` (init-only, `null` default) + `NativeAlarmTransition.AckTime` (trailing optional positional — all 14-arg call sites unchanged). - Proto: **field 24** `google.protobuf.Timestamp ack_time` on `AlarmStateUpdate`; regenerated with `docker/regen-proto.sh sitestream` (csproj diff verified empty). Packed/unpacked by `StreamRelayActor` / `SiteStreamGrpcClient`; an absent Timestamp round-trips to `null`. - Stamping rule (both protocols): non-null **only** while the condition is active AND acknowledged — that one predicate yields "null while unacked", "cleared on re-raise", and no phantom ack on the MxGateway return-to-normal (which maps `INACTIVE → Acknowledged = true`). Lives in the pure `OpcUaAlarmMapper.DeriveAckTime` / `MxGatewayAlarmMapper.DeriveAckTime`. - OPC UA gets a **true source ack instant**: new SelectClause **index 18** = `AcknowledgeableConditionType/AckedState/TransitionTime`, appended so indices 0–17 keep their meaning; falls back to the event's `Time` when the server omits it. MxGateway uses the ack transition's own timestamp (its feed carries no ack time), and an `ACTIVE_ACKED` re-subscribe snapshot restores one from `LastTransitionTimestamp`. - Persistence: rides `native_alarm_state`'s existing `metadata_json` blob, **not** a new column — that table is `RegisterReplicated` in `SiteLocalDbSetup` and LocalDb builds its CDC triggers from the column list at registration time, so an additive JSON property changes no schema, no triggers and no replication contract (`metadata_json` is exactly the extension point UA4 added). Pre-AckTime rows deserialize it as `null`. 2. ✅ **DONE 2026-08-01** — Add `AlarmsAccessor` + `ScriptAlarm` to the runtime context (`ScriptRuntimeContext`) — local Ask to the Instance Actor; project `AlarmStateChanged` → `ScriptAlarm` (incl. `AckTime`). Add internal request/response message if not reusing `DebugSnapshotRequest`. - Dedicated `GetAlarmSnapshotRequest`/`GetAlarmSnapshotResponse` (Commons `Messages/Instance`) rather than reusing `DebugSnapshotRequest`, which would materialise every attribute value on every alarm poll. Served from the same `BuildAlarmStatesSnapshot()` the Debug View uses, so the two can never disagree. - `AlarmsAccessor` sits in `ScopeAccessors.cs` beside the other accessors but is **not scope-prefixed** — alarm identity is not a scope-relative attribute name, so every scope sees the whole list. Exposed as `ScriptRuntimeContext.Alarms` and the top-level `ScriptGlobals.Alarms`. 3. ✅ **DONE 2026-08-01** — Mirror the stub on `ScriptCompileSurface` (and confirm `TriggerCompileSurface` not needed — trigger expressions don't read alarms). - `CompileAlarmsAccessor` returns the **same** `ScriptAlarm` type as the runtime (Commons is already in `DefaultAssemblies`), so field access binds identically at the gate and at the site. `TriggerCompileSurface` confirmed not needed. - Also mirrored on the **third** hand-maintained surface, the Central UI Test-Run `SandboxScriptHost` — without it the design page would false-flag CS1061 on scripts the deploy gate accepts. It throws a labelled `ScriptSandboxException` at run time (no central route to per-instance alarm state) rather than returning an empty list that would read as "nothing is in alarm". 4. ✅ **DONE 2026-08-01** — Confirm/extend `ScriptTrustPolicy` allow-list so `Alarms` is permitted; no new forbidden APIs. - **No change needed, and the reason is structural:** the trust boundary is a deny-list over API roots, not an allow-list of context members. Pinned by a test asserting no `ForbiddenScopes` entry prefixes the Commons script-surface namespace, so a future deny-list entry cannot silently make `ScriptAlarm` untouchable. 5. ✅ **DONE 2026-08-01** — Unit tests: runtime accessor projection (active/acked/severity/timestamps/AckTime), AckTime stamping + failover persistence, compile-surface compiles a representative `Alarms.CurrentAsync()` script, trust-policy accepts it. - New `AlarmsAccessorTests` (6), `NativeAlarmActor` AckTime emit/rehydrate/pre-AckTime-row (3), `InstanceActor` alarm-snapshot (2), mapper AckTime (4 OPC UA + 6 MxGateway), proto round-trip (1), Commons additive/back-compat (4), compile-surface + trust (4), `SandboxScriptHost` diagnose-clean (1). `AlarmsAccessor` added to the `CompileSurfaceParityTests` mirror pairs; the OPC UA SelectClause count lock-in went 18 → 19 with an index-18 assertion (intended — the clause is appended). 6. ✅ **DONE 2026-08-01** — Doc: update `Component-SiteRuntime.md` + `Component-DataConnectionLayer.md` (native-mirror AckTime) + Script Analysis #25 surface list; note the accessor in `Component-InboundAPI.md` routing examples. - Also updated the `CLAUDE.md` native-alarm bullet. The Inbound API note records the *negative* decision: no `Route.To(...).GetAlarms(...)` verb — alarm reads go through a routed site script so the filtering happens where the data lives. **Phase 2 — Inbound methods [deployed] + doc [repo]** 7. Update `SimpleAlarmStatusRequest` (id 9) body to the §5.1 router incl. the not-supported-machine catch (validate via design page first to avoid the stale-handler trap — see memory `inbound-noncompiling-update-keeps-old-handler`). 8. Create `AlarmStatus` method (params/return/script per §5.1); authorize the existing MES API key for both methods (§6.7). 9. Doc the two endpoints in `docs/requirements/Component-InboundAPI.md` (or a dedicated MES-integration note) cross-referencing the legacy spec. **Phase 3 — Site template scripts [deployed]** *(MESReceiver task removed — §6.2 decided dropped)* 10. Add `SimpleAlarmStatus` + `AlarmStatus` to **CvdReactor** (native sources via `Alarms.CurrentAsync()`, MES band constants), per §5.3. 11. `template validate` (script compilation gate) before redeploy; redeploy affected instances. **Phase 4 — Verify** 12. Build affected projects + run targeted tests (per memory `targeted-tests-not-full-suite`). 13. Live smoke against a real reactor instance (see §8) — both endpoints, success + machine-not-found + not-supported-machine + filtered. Requires the Galaxy MES-band priorities (§6.1) to be set for a meaningful flagged-only result. --- ## 8. Testing & verification - **Unit [repo]:** `Alarms` accessor projection; compile-surface acceptance; trust-policy allow; inbound param/return validation for `AlarmStatus` (nested objects). - **Design-page [deployed]:** paste both site scripts into the api-method/template design pages; confirm no CS errors and Test Run executes (this is the gate that catches `Database.QuerySingle`-style mistakes pre-deploy). - **Live smoke [deployed]:** `curl` `simplealarmstatus` and `alarmstatus` against the deployed reactor; cross-check returned alarms against the CLI debug snapshot (`debug snapshot --id `) alarm table for the same instance; verify only `Active` alarms appear, ack mapping, severity filtering, and machine-not-found error text. - **Caveat:** native MxGateway alarms need a real gateway; the docker cluster can't fully exercise the CvdReactor native path (cf. memory on no-local-gateway). Validate the native-read path on the wonder-app-vd03 / real-gateway environment; unit-test the projection in isolation. --- ## 9. Out of scope / follow-ups - ~~True `IsFlaggedForMES` allow-list~~ — resolved by the §6.1 severity-band decision (no follow-up needed). - ~~Precise `AckDT` follow-up~~ — pulled INTO scope by §6.4 (mirror enriched now). - A live/aggregated central alarm store or stream (the M7 follow-up) — these endpoints stay pull-based, per-instance, like the Alarm Summary page. - Alarm-status support for non-CvdReactor machine types (any future template just implements `SimpleAlarmStatus`/`AlarmStatus` root scripts against the same contract). --- ## 10. Summary of changes | Artifact | Type | Change | |---|---|---| | `AlarmStateChanged` + vendored `AlarmStateUpdate` proto (**field 24**) + `native_alarm_state` (`metadata_json`) + DCL stamping (OPC UA SelectClause **index 18**) | [repo] ✅ | Additive `AckTime` enrichment (§6.4) | | `ScriptRuntimeContext` + `ScriptGlobals` | [repo] ✅ | New `Alarms` accessor + `ScriptAlarm` (incl. `AckTime`); local alarm-snapshot Ask | | `ScriptCompileSurface` + Central UI `SandboxScriptHost` | [repo] ✅ | Mirror `Alarms` stub on both design-time surfaces | | `ScriptTrustPolicy` (#25) | [repo] ✅ | Verified — **no change needed** (deny-list, not member allow-list); pinned by test | | `GetAlarmSnapshotRequest`/`Response` (Commons) | [repo] ✅ | New, additive — chosen over reusing `DebugSnapshotRequest` | | Site Runtime + DCL + Script Analysis + Inbound API docs + `CLAUDE.md` | [repo] ✅ | Document `Alarms` accessor, `AckTime`, endpoints | | Unit tests (Commons / DCL / SiteRuntime / ScriptAnalysis / Communication / CentralUI) | [repo] ✅ | New | | `SimpleAlarmStatusRequest` (id 9) | [deployed] | Stub → real router (+ not-supported catch) | | `AlarmStatus` (new) | [deployed] | New inbound method, existing MES key authorized | | `CvdReactor.SimpleAlarmStatus` / `.AlarmStatus` | [deployed] | New native-source scripts (MES band 900–999) | | Galaxy alarm priorities | [external] | MES-relevant CvdReactor alarms configured into 900–999 (§6.1 prerequisite) |