From 1429ddaa0e31cb53991206a7ff7c37511f37cb3f Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 13 Jul 2026 01:45:50 -0400 Subject: [PATCH] docs(plans): add MES alarm-status API draft plan (2026-06-30) Ports the legacy WWSupport/APIServer MES alarm-status endpoints onto the Inbound API, mirroring the MesMoveIn/MesMoveOut pattern; draft for review, not yet executed. Committed ahead of PLAN-R2-08 T13's hygiene pass. --- docs/plans/2026-06-30-mes-alarm-status-api.md | 284 ++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 docs/plans/2026-06-30-mes-alarm-status-api.md diff --git a/docs/plans/2026-06-30-mes-alarm-status-api.md b/docs/plans/2026-06-30-mes-alarm-status-api.md new file mode 100644 index 00000000..42a12fd6 --- /dev/null +++ b/docs/plans/2026-06-30-mes-alarm-status-api.md @@ -0,0 +1,284 @@ +# MES Alarm-Status API — Implementation Plan + +**Date:** 2026-06-30 +**Status:** Draft plan — NOT yet executed. Captures design + task breakdown for review. +**Component touchpoints:** Inbound API (#14), Script Analysis (#25), Site Runtime (#3), Template Engine (#1) — plus deployed config (inbound methods + CvdReactor/MESReceiver 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 MES-receiver site scripts** (`SAPID → BTDB machine lookup → Route.To(code).Call(...)`), exactly like `MesMoveIn`. +4. The **generic version on the `MESReceiver` template** resolves the machine's alarms by **querying the BTDB `MachineAlarm` table** (the legacy approach). +5. The **`CvdReactor` template provides an override version** that reads the **alarms actually defined on the CvdReactor object** (its 7 native alarm-source bindings) directly — no `MachineAlarm` lookup. + +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 ┌────► MESReceiver.SimpleAlarmStatus (BTDB MachineAlarm) + ├─ Route.To(code).Call(...) ──┤ MESReceiver.AlarmStatus (BTDB MachineAlarm) +AlarmStatus (new) ─┘ (ScriptRuntimeContext + └────► CvdReactor.SimpleAlarmStatus (native sources — OVERRIDE) + ScriptCompileSurface) CvdReactor.AlarmStatus (native sources — OVERRIDE) +``` + +### 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. + +### 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, + string CurrentValue, string LimitValue, bool IsConfiguredPlaceholder); +``` + +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 (generic / "as before" — queries BTDB `MachineAlarm`):** `SimpleAlarmStatus` and `AlarmStatus`. +- Use `await Database.Connection("BTDB")` (raw ADO.NET — site Database helper has **no** `QuerySingleAsync`; that's inbound-only) to read the machine's `MachineAlarm` rows (`Name`, `Severity`, `FlaggedForMES`, …) for the resolved `MachineCode`. +- Apply legacy filter semantics (Simple = flagged-only; full = `FlaggedOnly/MinSeverity/MaxSeverity/NameFilter`). +- Determine live "triggered" state + ack/timestamps and project to `AlarmInfo` (**live-state source = open question §6.2**). +- **No `_A`/`_B` side parsing.** This generic version is **whole-machine**, matching the legacy endpoint — it accepts a bare/numeric SAPID and never requires (or errors on) a missing side suffix. Side routing is **CvdReactor-only** (§5.3). + +**CvdReactor (override — reads native sources directly, no DB):** `SimpleAlarmStatus` and `AlarmStatus` as **root-level scripts** (so `Route.To(code).Call("SimpleAlarmStatus")` resolves CvdReactor's version for CvdReactor instances — 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) +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: flagged-only + acked always included (see §6.1 for FlaggedForMES) + .Select(a => new { + Name = a.Name, + HierarchicalName = code + "." + a.Name, + Description = a.Message, // §6.3 + IsFlaggedForMES = true, // §6.1 + Severity = a.Severity, + StatusCode = a.Acknowledged ? "Triggered.Acked" : "Triggered", + TriggeredDT = (a.OriginalRaiseTime ?? a.Timestamp), + AckDT = (DateTime?)null, // §6.4 + 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 — but additionally applies the passed `AlarmFilter` (NameFilter/MinSeverity/MaxSeverity/FlaggedOnly/IncludeAcked) over the scoped native list. (When `AlarmStatus` was selected by `Code`/`ZTag`/`MachineID` with no SAPID suffix, `side == null`; in that case return all sources instead of erroring — see §6.6.) + +> **Shared `ReactorAlarms` (`Z28061.`) is included on both sides** — reactor-wide faults apply regardless of side. Flip this if MES wants strictly side-local alarms (drop the `StartsWith("Reactor")` clause). + +--- + +## 6. Key design decisions & OPEN QUESTIONS (resolve before execution) + +These are the Socratic checkpoints — they change script bodies and/or the contract. + +1. **`IsFlaggedForMES` for native alarms.** The native model has no MES flag (it lived in `MachineAlarm.FlaggedForMES`). Options: (a) treat all native alarms as MES-relevant (`true`); (b) reintroduce an MES allow-list (per native source binding, or a small config/table) the override consults; (c) for `SimpleAlarmStatus` (flagged-only) return all, and only honor `FlaggedOnly` in the full endpoint by cross-referencing `MachineAlarm`. **Recommend (a)** for v1 with (b) as a follow-up, unless MES needs a true flag. + +2. **MESReceiver live-state source.** MESReceiver has no native alarm sources, so after reading `MachineAlarm` config it has no live "InAlarm" source. Options: (a) MESReceiver version returns **configured alarms only** (no live triggered filter) — explicitly a degraded/reference path; (b) MESReceiver version ALSO reads `Alarms.CurrentAsync()` and cross-references `MachineAlarm` by name (works only if the instance happens to mirror native alarms); (c) declare the BTDB-only path returns config and document that live status requires the CvdReactor-style override. **Recommend (c)** — the real live path is CvdReactor; the MESReceiver version demonstrates the MachineAlarm-driven catalog and is the fallback for machine types catalogued in SQL. + +3. **`Description` mapping.** Legacy `Description` came from the MXAccess `DescAttrName` tag. `AlarmStateChanged` has no dedicated description; closest is `Message` (per-band operator text) / `AlarmTypeName` / `Category`. **Recommend `Message`, fall back to `AlarmTypeName`.** Confirm acceptable. + +4. **`AckDT` mapping.** No explicit ack-timestamp in `AlarmStateChanged` (`Timestamp` = last change, `OriginalRaiseTime` = raise). For acked alarms we can't precisely fill `AckDT`. Options: (a) leave `null`; (b) use `Timestamp` when `Acknowledged` (approximate); (c) enrich the native mirror to carry an ack timestamp (larger change). **Recommend (a)** for v1, (c) as a follow-up if MES depends on it. + +5. **Script naming / override resolution.** Keep script names **identical** across both templates (`SimpleAlarmStatus`, `AlarmStatus`) so the inbound method is template-agnostic (`Call("AlarmStatus")`), and CvdReactor's root-level definition is what `Route.Call` resolves for CvdReactor instances. (Alternative: `Mes`-prefixed CvdReactor scripts like `MesMoveIn`.) **Recommend identical names**; confirm `Route.Call` resolves a CvdReactor root script over any composed-module script of the same name (verify during impl — the MoveIn precedent says yes). + +6. **Side scoping — DECIDED, CvdReactor-only.** Only the **CvdReactor** override routes off the SAPID suffix: `_A` ⇒ Left, `_B` ⇒ Right; `_LT` is ignored (stripped before reading the side; leak-test sources still included). Mirrors `MesMoveIn`. The generic **MESReceiver** version and the inbound method do **not** parse or require the suffix — they stay whole-machine (legacy). Two residual sub-choices remain: (a) **shared `ReactorAlarms` (`Z28061.`) included on both sides** (current plan) vs strictly side-local — confirm; (b) **missing `_A`/`_B` suffix** — the **CvdReactor** `SimpleAlarmStatus` **errors** (matches `MesMoveIn`); the **CvdReactor** `AlarmStatus` selected by `Code`/`ZTag`/`MachineID` (no SAPID) **returns all sources** rather than erroring; the MESReceiver version never errors on a missing suffix — confirm acceptable. + +7. **New method roles / auth.** `AlarmStatus` is a new inbound `ApiMethod` (`X-API-Key`); creating it requires `Roles.Designer`. No new global roles. Confirm the API key in use is authorized. + +--- + +## 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 [repo]** +1. Add `AlarmsAccessor` + `ScriptAlarm` to the runtime context (`ScriptRuntimeContext`) — local Ask to the Instance Actor; project `AlarmStateChanged` → `ScriptAlarm`. Add internal request/response message if not reusing `DebugSnapshotRequest`. +2. Mirror the stub on `ScriptCompileSurface` (and confirm `TriggerCompileSurface` not needed — trigger expressions don't read alarms). +3. Confirm/extend `ScriptTrustPolicy` allow-list so `Alarms` is permitted; no new forbidden APIs. +4. Unit tests: runtime accessor projection (active/acked/severity/timestamps), compile-surface compiles a representative `Alarms.CurrentAsync()` script, trust-policy accepts it. +5. Doc: update `docs/requirements/Component-SiteRuntime.md` (+ Script Analysis #25 surface list) to document the `Alarms` accessor; note it in `Component-InboundAPI.md` routing examples. + +**Phase 2 — Inbound methods [deployed] + doc [repo]** +6. Update `SimpleAlarmStatusRequest` (id 9) body to the §5.1 router (validate via design page first to avoid the stale-handler trap — see memory `inbound-noncompiling-update-keeps-old-handler`). +7. Create `AlarmStatus` method (params/return/script per §5.1). +8. 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]** +9. Add `SimpleAlarmStatus` + `AlarmStatus` to **MESReceiver** (BTDB `MachineAlarm` via `Database.Connection("BTDB")`), per §6.2 decision. +10. Add override `SimpleAlarmStatus` + `AlarmStatus` to **CvdReactor** (native sources via `Alarms.CurrentAsync()`), per §5.3. +11. `template validate` both templates (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 + filtered. + +--- + +## 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` for native alarms (per-source MES allow-list) — §6.1 option (b). +- Precise `AckDT` (enrich native mirror with an ack timestamp) — §6.4 option (c). +- A live/aggregated central alarm store or stream (the M7 follow-up) — these endpoints stay pull-based, per-instance, like the Alarm Summary page. +- MESReceiver live-state path if §6.2 (c) is chosen and MES later needs live status from non-native machines. + +--- + +## 10. Summary of changes + +| Artifact | Type | Change | +|---|---|---| +| `ScriptRuntimeContext` | [repo] | New `Alarms` accessor + `ScriptAlarm`; local alarm-snapshot Ask | +| `ScriptCompileSurface` | [repo] | Mirror `Alarms` stub | +| `ScriptTrustPolicy` (#25) | [repo] | Allow `Alarms` member (verify) | +| Internal alarm-snapshot message (or reuse `DebugSnapshotRequest`) | [repo] | Additive | +| Site Runtime + Script Analysis + Inbound API docs | [repo] | Document `Alarms` accessor + endpoints | +| Unit tests (SiteRuntime / ScriptAnalysis / InboundAPI) | [repo] | New | +| `SimpleAlarmStatusRequest` (id 9) | [deployed] | Stub → real router | +| `AlarmStatus` (new) | [deployed] | New inbound method | +| `MESReceiver.SimpleAlarmStatus` / `.AlarmStatus` | [deployed] | New BTDB-driven scripts | +| `CvdReactor.SimpleAlarmStatus` / `.AlarmStatus` | [deployed] | New native-source override scripts |