feat(m9/T28b): trigger analysis-kind selector (UI) + --trigger-kind (CLI)

Surfaces the T28a backend "analysisKind" discriminator in both authoring
surfaces: an Advisory|Strict <select> (id="alarm-trigger-kind" /
"script-trigger-kind") added to the Expression fragment of
AlarmTriggerEditor and ScriptTriggerEditor, and a --trigger-kind option
on template alarm/script add+update in the CLI.

Key/value contract: "analysisKind":"Strict" when strict; key omitted for
Advisory — exactly as ValidationService.IsStrictAnalysis reads it.
Selector only shown for Expression triggers; non-Expression triggers do
not emit the key even if IsStrictAnalysisKind is set on the model.

Both projects build 0 warnings; 101 CentralUI Trigger tests + 33 CLI
Template tests pass.
This commit is contained in:
Joseph Doherty
2026-06-18 10:44:57 -04:00
parent f618ac0322
commit dcc6f623e2
8 changed files with 684 additions and 2 deletions
@@ -17,13 +17,33 @@ internal static class AlarmTriggerConfigJson
/// flags, or returns null when none are supplied (so the alarm is created without a
/// trigger config). Unknown/blank trigger types yield null.
/// </summary>
/// <param name="triggerType">The trigger type string (case-insensitive).</param>
/// <param name="attribute">Attribute name (non-Expression trigger types).</param>
/// <param name="matchValue">ValueMatch: value to compare against.</param>
/// <param name="notEquals">ValueMatch: invert the comparison.</param>
/// <param name="min">RangeViolation: minimum allowed value.</param>
/// <param name="max">RangeViolation: maximum allowed value.</param>
/// <param name="thresholdPerSecond">RateOfChange: rate threshold per second.</param>
/// <param name="windowSeconds">RateOfChange: sliding window in seconds.</param>
/// <param name="direction">RateOfChange: direction (rising|falling|either).</param>
/// <param name="loLo">HiLo: low-low setpoint.</param>
/// <param name="lo">HiLo: low setpoint.</param>
/// <param name="hi">HiLo: high setpoint.</param>
/// <param name="hiHi">HiLo: high-high setpoint.</param>
/// <param name="expression">Expression: boolean trigger expression.</param>
/// <param name="analysisKind">
/// M9-T28b: optional analysis kind for Expression triggers ("strict" → emits
/// <c>"analysisKind":"Strict"</c>; null/"advisory"/anything else → Advisory default,
/// key omitted). Ignored for non-Expression trigger types.
/// </param>
internal static string? Build(
string triggerType, string? attribute,
string? matchValue, bool notEquals,
double? min, double? max,
double? thresholdPerSecond, double? windowSeconds, string? direction,
double? loLo, double? lo, double? hi, double? hiHi,
string? expression)
string? expression,
string? analysisKind = null)
{
var type = triggerType?.Trim();
var anyTyped = attribute is not null || matchValue is not null || notEquals
@@ -66,6 +86,11 @@ internal static class AlarmTriggerConfigJson
break;
case "expression":
w.WriteString("expression", expression ?? "");
// M9-T28b: emit "analysisKind":"Strict" only when the caller passes
// --trigger-kind strict (case-insensitive); Advisory (the default) is
// expressed by omitting the key, matching ValidationService.IsStrictAnalysis.
if (string.Equals(analysisKind?.Trim(), "Strict", StringComparison.OrdinalIgnoreCase))
w.WriteString("analysisKind", "Strict");
break;
}
w.WriteEndObject();