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:
@@ -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(
|
||||
|
||||
@@ -100,6 +100,15 @@ internal static class AlarmTriggerConfigCodec
|
||||
|
||||
case AlarmTriggerType.Expression:
|
||||
model.Expression = TryReadString(root, "expression");
|
||||
// M9-T28b: read optional analysisKind discriminator ("Strict" → strict;
|
||||
// absent/"Advisory"/anything else → Advisory default, preserving
|
||||
// today's behavior exactly — matches ValidationService.IsStrictAnalysis).
|
||||
if (root.TryGetProperty("analysisKind", out var ak)
|
||||
&& ak.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
model.IsStrictAnalysisKind = string.Equals(
|
||||
ak.GetString(), "Strict", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -172,6 +181,11 @@ internal static class AlarmTriggerConfigCodec
|
||||
|
||||
case AlarmTriggerType.Expression:
|
||||
w.WriteString("expression", model.Expression ?? "");
|
||||
// M9-T28b: emit "analysisKind":"Strict" only when explicitly set;
|
||||
// Advisory is the default so the key is omitted to keep the payload
|
||||
// minimal and backward-compatible with older ValidationService versions.
|
||||
if (model.IsStrictAnalysisKind)
|
||||
w.WriteString("analysisKind", "Strict");
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -342,4 +356,14 @@ internal sealed class AlarmTriggerModel
|
||||
/// The boolean C# expression to evaluate for Expression triggers.
|
||||
/// </summary>
|
||||
public string? Expression { get; set; }
|
||||
|
||||
// M9-T28b: per-trigger analysis kind (Expression only). When true the
|
||||
// codec serializes "analysisKind":"Strict"; when false (Advisory, the
|
||||
// default) the key is omitted. Matches ValidationService.IsStrictAnalysis.
|
||||
/// <summary>
|
||||
/// When <see langword="true"/>, the trigger config carries
|
||||
/// <c>"analysisKind":"Strict"</c> so ValidationService escalates the
|
||||
/// blank-expression advisory to a deploy-blocking error.
|
||||
/// </summary>
|
||||
public bool IsStrictAnalysisKind { get; set; }
|
||||
}
|
||||
|
||||
@@ -534,6 +534,8 @@
|
||||
_thresholdText = FormatNullable(_model.ThresholdPerSecond);
|
||||
_windowText = FormatNullable(_model.WindowSeconds);
|
||||
_directionText = _model.Direction;
|
||||
// M9-T28b: sync the analysis-kind selector from the loaded model.
|
||||
_analysisKindValue = _model.IsStrictAnalysisKind ? "Strict" : "Advisory";
|
||||
_loLoText = FormatNullable(_model.LoLo);
|
||||
_loText = FormatNullable(_model.Lo);
|
||||
_hiText = FormatNullable(_model.Hi);
|
||||
@@ -583,6 +585,21 @@
|
||||
<div class="form-text">
|
||||
A boolean C# expression — e.g. <code>Attributes["Temperature"] > 80</code>.
|
||||
</div>
|
||||
@* M9-T28b: analysis-kind selector — Advisory (default) keeps the blank-expression
|
||||
finding as a non-blocking warning; Strict escalates it to a deploy-blocking error. *@
|
||||
<div class="mt-2">
|
||||
<label for="alarm-trigger-kind" class="form-label small text-uppercase text-muted fw-semibold mb-1">
|
||||
Analysis kind
|
||||
</label>
|
||||
<select id="alarm-trigger-kind"
|
||||
class="form-select form-select-sm"
|
||||
style="max-width: 280px;"
|
||||
@bind="_analysisKindValue"
|
||||
@bind:after="OnAnalysisKindChanged">
|
||||
<option value="Advisory">Advisory — blank expression is a warning</option>
|
||||
<option value="Strict">Strict — blank expression blocks deploy</option>
|
||||
</select>
|
||||
</div>
|
||||
};
|
||||
|
||||
private async Task OnExpressionChanged(string value)
|
||||
@@ -591,6 +608,15 @@
|
||||
await Emit();
|
||||
}
|
||||
|
||||
// M9-T28b: backing field + handler for the Advisory|Strict selector.
|
||||
private string _analysisKindValue = "Advisory";
|
||||
|
||||
private async Task OnAnalysisKindChanged()
|
||||
{
|
||||
_model.IsStrictAnalysisKind = string.Equals(_analysisKindValue, "Strict", StringComparison.OrdinalIgnoreCase);
|
||||
await Emit();
|
||||
}
|
||||
|
||||
// ── Hint text ──────────────────────────────────────────────────────────
|
||||
|
||||
private string BuildHint()
|
||||
|
||||
@@ -42,6 +42,16 @@ internal sealed class ScriptTriggerModel
|
||||
|
||||
/// <summary>Fire mode (Conditional + Expression). Defaults to <see cref="ScriptTriggerMode.OnTrue"/>.</summary>
|
||||
public ScriptTriggerMode Mode { get; set; } = ScriptTriggerMode.OnTrue;
|
||||
|
||||
// M9-T28b: per-trigger analysis kind (Expression only). When true the
|
||||
// codec serializes "analysisKind":"Strict"; when false (Advisory, the
|
||||
// default) the key is omitted. Matches ValidationService.IsStrictAnalysis.
|
||||
/// <summary>
|
||||
/// When <see langword="true"/>, the trigger config carries
|
||||
/// <c>"analysisKind":"Strict"</c> so ValidationService escalates the
|
||||
/// blank-expression advisory to a deploy-blocking error.
|
||||
/// </summary>
|
||||
public bool IsStrictAnalysisKind { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -148,6 +158,15 @@ internal static class ScriptTriggerConfigCodec
|
||||
case ScriptTriggerKind.Expression:
|
||||
model.Expression = root.TryGetProperty("expression", out var e) ? e.GetString() : null;
|
||||
model.Mode = ReadMode(root);
|
||||
// M9-T28b: read optional analysisKind discriminator (matches
|
||||
// ValidationService.IsStrictAnalysis — "Strict" case-insensitive → strict;
|
||||
// absent/"Advisory"/anything else → Advisory default).
|
||||
if (root.TryGetProperty("analysisKind", out var ak)
|
||||
&& ak.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
model.IsStrictAnalysisKind = string.Equals(
|
||||
ak.GetString(), "Strict", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -196,6 +215,10 @@ internal static class ScriptTriggerConfigCodec
|
||||
case ScriptTriggerKind.Expression:
|
||||
w.WriteString("expression", model.Expression ?? "");
|
||||
w.WriteString("mode", model.Mode.ToString());
|
||||
// M9-T28b: emit "analysisKind":"Strict" only when explicitly set;
|
||||
// Advisory is the default so the key is omitted to stay backward-compatible.
|
||||
if (model.IsStrictAnalysisKind)
|
||||
w.WriteString("analysisKind", "Strict");
|
||||
break;
|
||||
|
||||
// Call → empty object.
|
||||
|
||||
@@ -136,6 +136,8 @@
|
||||
_operator = _model.Operator;
|
||||
_thresholdText = _model.Threshold?.ToString("R", CultureInfo.InvariantCulture);
|
||||
(_intervalText, _intervalUnit) = SplitInterval(_model.IntervalMs);
|
||||
// M9-T28b: sync the analysis-kind selector from the loaded model.
|
||||
_analysisKindValue = _model.IsStrictAnalysisKind ? "Strict" : "Advisory";
|
||||
}
|
||||
|
||||
/// <summary>Chooses the largest whole unit (min/sec/ms) that represents the period exactly.</summary>
|
||||
@@ -272,6 +274,21 @@
|
||||
<div class="form-text">
|
||||
A boolean C# expression — e.g. <code>Attributes["Temperature"] > 80</code>.
|
||||
</div>
|
||||
@* M9-T28b: analysis-kind selector — Advisory keeps the blank-expression finding
|
||||
as a non-blocking warning; Strict escalates it to a deploy-blocking error. *@
|
||||
<div class="mt-2">
|
||||
<label for="script-trigger-kind" class="form-label small text-uppercase text-muted fw-semibold mb-1">
|
||||
Analysis kind
|
||||
</label>
|
||||
<select id="script-trigger-kind"
|
||||
class="form-select form-select-sm"
|
||||
style="max-width: 280px;"
|
||||
@bind="_analysisKindValue"
|
||||
@bind:after="OnAnalysisKindChanged">
|
||||
<option value="Advisory">Advisory — blank expression is a warning</option>
|
||||
<option value="Strict">Strict — blank expression blocks deploy</option>
|
||||
</select>
|
||||
</div>
|
||||
};
|
||||
|
||||
private async Task OnExpressionChanged(string value)
|
||||
@@ -280,6 +297,15 @@
|
||||
await Emit();
|
||||
}
|
||||
|
||||
// M9-T28b: backing field + handler for the Advisory|Strict selector.
|
||||
private string _analysisKindValue = "Advisory";
|
||||
|
||||
private async Task OnAnalysisKindChanged()
|
||||
{
|
||||
_model.IsStrictAnalysisKind = string.Equals(_analysisKindValue, "Strict", StringComparison.OrdinalIgnoreCase);
|
||||
await Emit();
|
||||
}
|
||||
|
||||
// ── Fire mode (Conditional + Expression) ───────────────────────────────
|
||||
|
||||
private RenderFragment RenderMode() => __builder =>
|
||||
|
||||
Reference in New Issue
Block a user