feat(scripted-alarms): DeploymentArtifact byte-parity for the alarm plan (T6)

This commit is contained in:
Joseph Doherty
2026-06-10 14:41:46 -04:00
parent 55101baaa4
commit 8e8ca9efe8
4 changed files with 376 additions and 45 deletions
@@ -1,6 +1,5 @@
using System.Diagnostics;
using System.Text.Json;
using System.Text.RegularExpressions;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
@@ -208,10 +207,10 @@ public sealed record EquipmentScriptedAlarmPlan(
/// diffs empty (mirrors <see cref="EquipmentVirtualTagPlan"/>).</summary>
/// <remarks>
/// <b>DependencyRefs equality is order-sensitive</b> (SequenceEqual).
/// <see cref="Phase7Composer.MergeAlarmDependencyRefs"/> is the canonical, deterministic
/// <see cref="EquipmentScriptPaths.ExtractAlarmDependencyRefs"/> is the canonical, deterministic
/// producer of that order (predicate <c>ctx.GetTag</c> reads first, then first-seen message
/// template tokens). Downstream byte-parity between the live composer and the artifact-decode
/// mirror depends on both sides calling <c>MergeAlarmDependencyRefs</c> with identical inputs.
/// mirror depends on both sides calling <c>ExtractAlarmDependencyRefs</c> with identical inputs.
/// </remarks>
public bool Equals(EquipmentScriptedAlarmPlan? other) =>
other is not null &&
@@ -452,7 +451,10 @@ public static class Phase7Composer
MessageTemplate: a.MessageTemplate,
PredicateScriptId: a.PredicateScriptId,
PredicateSource: source,
DependencyRefs: MergeAlarmDependencyRefs(source, a.MessageTemplate),
// Scripted alarms do NOT use {{equip}} substitution (only virtual tags do) — pass the
// predicate source as-is. The merge (predicate reads first, then template tokens) lives
// in the shared EquipmentScriptPaths helper so the artifact-decode mirror agrees.
DependencyRefs: EquipmentScriptPaths.ExtractAlarmDependencyRefs(source, a.MessageTemplate),
HistorizeToAveva: a.HistorizeToAveva,
Retain: a.Retain,
Enabled: a.Enabled));
@@ -466,47 +468,6 @@ public static class Phase7Composer
};
}
// {TagPath} message-template token — a single-brace {…} run. Reuses no existing helper because
// EquipmentScriptPaths only handles {{equip}} + ctx.GetTag literals; this is the alarm-message
// surface. The negative look-behind/-ahead reject the reserved {{equip}} double-brace form
// (and any other doubled brace), so "{A.X}" matches but "{{equip}}" does not yield "{equip}".
private static readonly Regex MessageTemplateTokenRegex =
new(@"(?<!\{)\{([^{}]+)\}(?!\})", RegexOptions.Compiled);
/// <summary>
/// Merge the alarm's dependency graph: the predicate script's distinct <c>ctx.GetTag("…")</c>
/// read refs (first-seen order) UNION the distinct <c>{TagPath}</c> token paths referenced in
/// the message template (first-seen order, appended after the predicate reads). The reserved
/// <c>{{equip}}</c> double-brace form is excluded by the token regex. Deterministic so the
/// artifact-decode mirror (sibling task) can reproduce the exact same ordered list.
/// </summary>
/// <param name="predicateSource">The resolved predicate script source.</param>
/// <param name="messageTemplate">The alarm message template carrying <c>{TagPath}</c> tokens.</param>
/// <returns>The merged, distinct, deterministically-ordered dependency refs.</returns>
private static IReadOnlyList<string> MergeAlarmDependencyRefs(string predicateSource, string? messageTemplate)
{
var seen = new HashSet<string>(StringComparer.Ordinal);
var result = new List<string>();
// Predicate reads first — same regex extraction the VirtualTag projection uses.
foreach (var r in EquipmentScriptPaths.ExtractDependencyRefs(predicateSource))
{
if (seen.Add(r)) result.Add(r);
}
// Then message-template {TagPath} tokens (first-seen), trimmed, non-empty.
if (!string.IsNullOrEmpty(messageTemplate))
{
foreach (Match m in MessageTemplateTokenRegex.Matches(messageTemplate))
{
var token = m.Groups[1].Value.Trim();
if (token.Length > 0 && seen.Add(token)) result.Add(token);
}
}
return result;
}
/// <summary>
/// Extract the driver-side full reference from a <see cref="Tag.TagConfig"/> JSON blob: the
/// <c>CK_Tag_TagConfig_IsJson</c> constraint guarantees a JSON object, and every shipped