Files
ScadaBridge/docs/plans/2026-06-30-mes-alarm-status-api.md
T
Joseph Doherty 1429ddaa0e 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.
2026-07-13 01:45:50 -04:00

285 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` (0999), `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<Object>() };`. 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<string>("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<AlarmStateChanged> 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(<siteScript>, <filters>)` 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<object>() };
var sap = m.Groups[1].Value; // numeric SAP for the DB lookup
var code = await Database.QuerySingleAsync<string>("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<object>() };
// 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<object>() };
}
```
**`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 = <raw MachineFilter.SAPID or "">, ...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<IReadOnlyList<ScriptAlarm>> 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<object>() };
// Keep this side's sources (Left*/Right*) + the shared reactor-wide source; drop the other side's.
System.Func<string,bool> 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<object>() };
}
```
`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 <instance>`) 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 |