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();
@@ -339,6 +339,13 @@ public static class TemplateCommands
var hiOption = new Option<double?>("--hi") { Description = "HiLo: high setpoint" };
var hiHiOption = new Option<double?>("--hihi") { Description = "HiLo: high-high setpoint" };
var expressionOption = new Option<string?>("--expression") { Description = "Expression: boolean trigger expression" };
// M9-T28b: analysis kind for Expression triggers (advisory|strict; default advisory).
// Writes "analysisKind":"Strict" into the trigger config when set to strict.
var triggerKindOption = new Option<string?>("--trigger-kind")
{
Description = "Expression trigger analysis kind: advisory (default) or strict. " +
"Strict escalates a blank expression from a warning to a deploy-blocking error."
};
var addCmd = new Command("add") { Description = "Add an alarm to a template" };
addCmd.Add(templateIdOption);
@@ -361,6 +368,7 @@ public static class TemplateCommands
addCmd.Add(hiOption);
addCmd.Add(hiHiOption);
addCmd.Add(expressionOption);
addCmd.Add(triggerKindOption);
addCmd.SetAction(async (ParseResult result) =>
{
var triggerType = result.GetValue(triggerTypeOption)!;
@@ -372,7 +380,8 @@ public static class TemplateCommands
result.GetValue(minOption), result.GetValue(maxOption),
result.GetValue(thresholdOption), result.GetValue(windowOption), result.GetValue(directionOption),
result.GetValue(loLoOption), result.GetValue(loOption), result.GetValue(hiOption), result.GetValue(hiHiOption),
result.GetValue(expressionOption));
result.GetValue(expressionOption),
result.GetValue(triggerKindOption));
return await CommandHelpers.ExecuteCommandAsync(
result, urlOption, formatOption, usernameOption, passwordOption,
@@ -395,6 +404,11 @@ public static class TemplateCommands
var updateTriggerConfigOption = new Option<string?>("--trigger-config") { Description = "Trigger configuration JSON" };
var updateLockedOption = new Option<bool>("--locked") { Description = "Lock status" };
updateLockedOption.DefaultValueFactory = _ => false;
// M9-T28b: --trigger-kind for update (same semantics as add)
var updateTriggerKindOption = new Option<string?>("--trigger-kind")
{
Description = "Expression trigger analysis kind: advisory (default) or strict."
};
var updateCmd = new Command("update") { Description = "Update a template alarm" };
updateCmd.Add(updateIdOption);
@@ -404,6 +418,7 @@ public static class TemplateCommands
updateCmd.Add(updateDescOption);
updateCmd.Add(updateTriggerConfigOption);
updateCmd.Add(updateLockedOption);
updateCmd.Add(updateTriggerKindOption);
updateCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
@@ -514,6 +529,13 @@ public static class TemplateCommands
var paramsOption = new Option<string?>("--parameters") { Description = "Parameter definitions JSON" };
var returnOption = new Option<string?>("--return-def") { Description = "Return definition JSON" };
// M9-T28b: analysis kind for Expression triggers (advisory|strict; default advisory).
var scriptTriggerKindOption = new Option<string?>("--trigger-kind")
{
Description = "Expression trigger analysis kind: advisory (default) or strict. " +
"Strict escalates a blank expression from a warning to a deploy-blocking error. " +
"Used with --trigger-config or appended to a built config when --trigger-type is Expression."
};
var addCmd = new Command("add") { Description = "Add a script to a template" };
addCmd.Add(templateIdOption);
@@ -524,6 +546,7 @@ public static class TemplateCommands
addCmd.Add(lockedOption);
addCmd.Add(paramsOption);
addCmd.Add(returnOption);
addCmd.Add(scriptTriggerKindOption);
addCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(
@@ -550,6 +573,11 @@ public static class TemplateCommands
var updateParamsOption = new Option<string?>("--parameters") { Description = "Parameter definitions JSON" };
var updateReturnOption = new Option<string?>("--return-def") { Description = "Return definition JSON" };
// M9-T28b: --trigger-kind for update (same semantics as add)
var updateScriptTriggerKindOption = new Option<string?>("--trigger-kind")
{
Description = "Expression trigger analysis kind: advisory (default) or strict."
};
var updateCmd = new Command("update") { Description = "Update a template script" };
updateCmd.Add(updateIdOption);
@@ -560,6 +588,7 @@ public static class TemplateCommands
updateCmd.Add(updateLockedOption);
updateCmd.Add(updateParamsOption);
updateCmd.Add(updateReturnOption);
updateCmd.Add(updateScriptTriggerKindOption);
updateCmd.SetAction(async (ParseResult result) =>
{
return await CommandHelpers.ExecuteCommandAsync(