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
@@ -20,6 +20,12 @@ public static class EquipmentScriptPaths
private static readonly Regex GetTagRefRegex =
new(@"ctx\s*\.\s*GetTag\s*\(\s*""([^""]+)""\s*\)", RegexOptions.Compiled);
// {TagPath} message-template token — a single-brace {…} run. 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);
// ctx.GetTag("…") OR ctx.SetVirtualTag("…", …) — first string-literal arg captured in
// three parts (prefix, content, closing quote) so token substitution touches ONLY the
// literal content (never a comment, Logger string, or other code).
@@ -92,4 +98,42 @@ public static class EquipmentScriptPaths
}
return result;
}
/// <summary>
/// Merge a scripted alarm's dependency graph: the predicate script's distinct
/// <c>ctx.GetTag("…")</c> read refs (via <see cref="ExtractDependencyRefs"/>, first-seen order)
/// UNION the distinct <c>{TagPath}</c> token paths referenced in the message template (first-seen
/// order, appended after the predicate reads, trimmed + non-empty). The reserved
/// <c>{{equip}}</c> double-brace form is excluded by the token regex. Deterministic so the live
/// composer (<c>Phase7Composer</c>) and the artifact-decode mirror (<c>DeploymentArtifact</c>)
/// produce the exact same ordered list — the byte-parity contract <c>EquipmentScriptedAlarmPlan</c>
/// equality depends on. Scripted alarms do NOT use <c>{{equip}}</c> substitution (only virtual
/// tags do) — pass the predicate source as-is.
/// </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>
public static IReadOnlyList<string> ExtractAlarmDependencyRefs(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 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;
}
}