Files
lmxopcua/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmConditionState.cs
T
Joseph Doherty 99354bfaf2 fix(core-scripted-alarms): resolve Low code-review findings (Core.ScriptedAlarms-003,006,008,010,011; -009 documented)
- Core.ScriptedAlarms-003: emit OnEvent OUTSIDE _evalGate by collecting
  pending emissions during the gate-held section and flushing them after
  release; eliminates re-entrancy deadlock the docs already promised.
- Core.ScriptedAlarms-006: track every fire-and-forget Reevaluate /
  ShelvingCheck task in _inFlight; Dispose drains the set so the engine
  no longer races store writes against teardown.
- Core.ScriptedAlarms-008: store comments as ImmutableList<AlarmComment>
  so AppendComment is O(log n) instead of O(n).
- Core.ScriptedAlarms-010: document the deliberate input-quality
  asymmetry (Uncertain drives the predicate, renders {?} in the message)
  in docs/ScriptedAlarms.md and on MessageTemplate.Resolve remarks.
- Core.ScriptedAlarms-011: propagate the no-op reason through
  TransitionResult.NoOp(state, reason) and log it from
  ScriptedAlarmEngine.ApplyAsync.
- Core.ScriptedAlarms-009 (Won't Fix per recommendation): documented the
  per-evaluation dictionary allocation in docs/v2/Galaxy.Performance.md
  with a mitigation path if a future soak surfaces pressure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 07:23:31 -04:00

90 lines
3.9 KiB
C#

using System.Collections.Immutable;
namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
/// <summary>
/// Persistent per-alarm state tracked by the Part 9 state machine. Every field
/// carried here either participates in the state machine or contributes to the
/// audit trail required by Phase 7 plan decision #14 (GxP / 21 CFR Part 11).
/// </summary>
/// <remarks>
/// <para>
/// <see cref="Active"/> is re-derived from the predicate at startup per Phase 7
/// decision #14 — the engine runs every alarm's predicate against current tag
/// values at <c>Load</c>, overriding whatever Active state is in the store.
/// Every other state field persists verbatim across server restarts so
/// operators don't re-ack active alarms after an outage + shelved alarms stay
/// shelved + audit history survives.
/// </para>
/// <para>
/// <see cref="Comments"/> is append-only; comments + ack/confirm user identities
/// are the audit surface regulators consume. The engine never rewrites past
/// entries. The runtime type is <see cref="ImmutableList{AlarmComment}"/> so
/// each append is O(log n) rather than the O(n) copy a plain
/// <c>IReadOnlyList&lt;AlarmComment&gt;</c> would force on every audit-producing
/// transition. (Core.ScriptedAlarms-008)
/// </para>
/// </remarks>
public sealed record AlarmConditionState(
string AlarmId,
AlarmEnabledState Enabled,
AlarmActiveState Active,
AlarmAckedState Acked,
AlarmConfirmedState Confirmed,
ShelvingState Shelving,
DateTime LastTransitionUtc,
DateTime? LastActiveUtc,
DateTime? LastClearedUtc,
DateTime? LastAckUtc,
string? LastAckUser,
string? LastAckComment,
DateTime? LastConfirmUtc,
string? LastConfirmUser,
string? LastConfirmComment,
ImmutableList<AlarmComment> Comments)
{
/// <summary>Initial-load state for a newly registered alarm — everything in the "no-event" position.</summary>
public static AlarmConditionState Fresh(string alarmId, DateTime nowUtc) => new(
AlarmId: alarmId,
Enabled: AlarmEnabledState.Enabled,
Active: AlarmActiveState.Inactive,
Acked: AlarmAckedState.Acknowledged,
Confirmed: AlarmConfirmedState.Confirmed,
Shelving: ShelvingState.Unshelved,
LastTransitionUtc: nowUtc,
LastActiveUtc: null,
LastClearedUtc: null,
LastAckUtc: null,
LastAckUser: null,
LastAckComment: null,
LastConfirmUtc: null,
LastConfirmUser: null,
LastConfirmComment: null,
Comments: ImmutableList<AlarmComment>.Empty);
}
/// <summary>
/// Shelving state — kind plus, for <see cref="ShelvingKind.Timed"/>, the UTC
/// timestamp at which the shelving auto-expires. The engine polls the timer on its
/// evaluation cadence; callers should not rely on millisecond-precision expiry.
/// </summary>
public sealed record ShelvingState(ShelvingKind Kind, DateTime? UnshelveAtUtc)
{
public static readonly ShelvingState Unshelved = new(ShelvingKind.Unshelved, null);
}
/// <summary>
/// A single append-only audit record — acknowledgement / confirmation / explicit
/// comment / shelving action. Every entry carries a monotonic UTC timestamp plus the
/// user identity Phase 6.2 authenticated.
/// </summary>
/// <param name="TimestampUtc">When the action happened.</param>
/// <param name="User">OS / LDAP identity of the actor. For engine-internal events (shelving expiry, startup recovery) this is <c>"system"</c>.</param>
/// <param name="Kind">Human-readable classification — "Acknowledge", "Confirm", "ShelveOneShot", "ShelveTimed", "Unshelve", "AddComment", "Enable", "Disable", "AutoUnshelve".</param>
/// <param name="Text">Operator-supplied comment or engine-generated message.</param>
public sealed record AlarmComment(
DateTime TimestampUtc,
string User,
string Kind,
string Text);