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.
This commit is contained in:
Joseph Doherty
2026-08-01 13:12:46 -04:00
parent d0af884760
commit 3c9b101dfc
5 changed files with 50 additions and 18 deletions
+28 -15
View File
@@ -1,7 +1,7 @@
# MES Alarm-Status API — Implementation Plan
**Date:** 2026-06-30
**Status:** Ready to execute — all §6 open questions DECIDED 2026-08-01 (design review with user). Not yet implemented.
**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).
---
@@ -234,13 +234,26 @@ try {
> 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]**
1. **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).
2. 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`.
3. Mirror the stub on `ScriptCompileSurface` (and confirm `TriggerCompileSurface` not needed — trigger expressions don't read alarms).
4. Confirm/extend `ScriptTrustPolicy` allow-list so `Alarms` is permitted; no new forbidden APIs.
5. 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.
6. 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.
**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 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 `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`).
@@ -279,13 +292,13 @@ try {
| Artifact | Type | Change |
|---|---|---|
| `AlarmStateChanged` + vendored `AlarmStateUpdate` proto + `native_alarm_state` + DCL stamping | [repo] | Additive `AckTime` enrichment (§6.4) |
| `ScriptRuntimeContext` | [repo] | New `Alarms` accessor + `ScriptAlarm` (incl. `AckTime`); 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 + DCL + Script Analysis + Inbound API docs | [repo] | Document `Alarms` accessor, `AckTime`, endpoints |
| Unit tests (SiteRuntime / ScriptAnalysis / InboundAPI) | [repo] | New |
| `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) |