Files
ScadaBridge/docs/plans/2026-06-30-mes-alarm-status-api.md
T
Joseph Doherty 3c9b101dfc docs(alarms): document the Alarms accessor + AckTime, tick MES plan Phase 1
Design doc and code travel together (CLAUDE.md editing rules), so this records
what the two preceding commits shipped and, more usefully, WHY the non-obvious
choices were made -- the parts a future reader would otherwise re-litigate:

  - Component-SiteRuntime.md: the Alarms.CurrentAsync() runtime API entry (why
    it is not scope-prefixed, why it is read-only, why placeholder rows are
    included), the full ScriptAlarm shape, AckTime on the enriched
    AlarmStateChanged, proto field 24, and the metadata_json-vs-new-column
    persistence rationale (native_alarm_state is RegisterReplicated; LocalDb
    builds its CDC triggers from the column list at registration time).
  - Component-DataConnectionLayer.md already carried the AckTime section in the
    first commit; this adds the SiteRuntime/ScriptAnalysis/InboundAPI halves.
  - Component-ScriptAnalysis.md: accessors returning domain types return the
    SAME type on both surfaces, and the trust-model note that a deny-list needs
    no entry for a new globals member -- only that its return type resolves in
    a permitted namespace.
  - Component-InboundAPI.md records the NEGATIVE decision: there is
    deliberately no Route.To(...).GetAlarms(...) verb. Alarm state is
    per-instance and lives on the site's Instance Actor, so the read goes
    through a routed site script and the filtering happens where the data is;
    central stays a thin router.
  - CLAUDE.md native-alarm bullet gains the enrichment + accessor summary.
  - The plan's §7 Phase 1 rows are ticked with 2026-08-01 and annotated with
    what was actually built (incl. the two choices that differ from the plan's
    "or" options: a dedicated snapshot message rather than DebugSnapshotRequest,
    and the extra SandboxScriptHost mirror the plan did not list). Phases 2-4
    stay open -- they are deployed config and need a live rig.
2026-08-01 13:12:46 -04:00

31 KiB
Raw Blame History

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 24 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.


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 TemplateAlarms.
  • 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
                           ├─ 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(<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.

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.

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):

// 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,
    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 AlarmStateChangedScriptAlarm). 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
// 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<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 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<object>() };
}

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 900999 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 900999 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-01AckTime 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 017 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 AlarmStateChangedScriptAlarm (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 <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 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 900999)
Galaxy alarm priorities [external] MES-relevant CvdReactor alarms configured into 900999 (§6.1 prerequisite)