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:
@@ -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