fix(client): offer exactly the served aggregates in the CLI and UI

R08 2.3: AggregateType drops StandardDeviation (no backend serves it; always
BadAggregateNotSupported) and gains Range, Delta, MinimumActualTime,
MaximumActualTime, PercentGood and PercentBad. A parity test pins the enum to
AggregateFamilies.Served and each member to its same-named Part 13 ObjectId.
The CLI parses every enum name (plus aliases) and the UI picker lists them all.
Existing members keep their numeric values; 6 is retired.

Refs #747
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 16:24:18 -04:00
parent 57ae69a05e
commit a74a18e49a
9 changed files with 200 additions and 62 deletions
@@ -47,7 +47,7 @@ public class HistoryReadCommand : CommandBase
/// <summary>
/// Gets the optional aggregate name used when the operator wants processed history instead of raw samples.
/// </summary>
[CommandOption("aggregate", Description = "Aggregate function: Average, Minimum, Maximum, Count, Start, End, StandardDeviation, TimeAverage, TimeAverage2, Total, Total2, Interpolative")]
[CommandOption("aggregate", Description = "Aggregate function: Average, Minimum, Maximum, Count, Start, End, TimeAverage, TimeAverage2, Total, Total2, Interpolative, Range, Delta, MinimumActualTime, MaximumActualTime, PercentGood, PercentBad")]
public string? Aggregate { get; init; }
/// <summary>
@@ -199,24 +199,33 @@ public class HistoryReadCommand : CommandBase
}
}
private static readonly string SupportedAggregates = string.Join(", ", Enum.GetNames<AggregateType>());
private static AggregateType ParseAggregateType(string name)
{
return name.Trim().ToLowerInvariant() switch
var trimmed = name.Trim();
var alias = trimmed.ToLowerInvariant() switch
{
"average" or "avg" => AggregateType.Average,
"minimum" or "min" => AggregateType.Minimum,
"maximum" or "max" => AggregateType.Maximum,
"count" => AggregateType.Count,
"start" or "first" => AggregateType.Start,
"end" or "last" => AggregateType.End,
"standarddeviation" or "stddev" or "stdev" => AggregateType.StandardDeviation,
"timeaverage" or "timeavg" => AggregateType.TimeAverage,
"timeaverage2" or "timeavg2" => AggregateType.TimeAverage2,
"total" => AggregateType.Total,
"total2" => AggregateType.Total2,
"interpolative" or "interp" => AggregateType.Interpolative,
_ => throw new ArgumentException(
$"Unknown aggregate: '{name}'. Supported: Average, Minimum, Maximum, Count, Start, End, StandardDeviation, TimeAverage, TimeAverage2, Total, Total2, Interpolative")
"avg" => AggregateType.Average,
"min" => AggregateType.Minimum,
"max" => AggregateType.Maximum,
"first" => AggregateType.Start,
"last" => AggregateType.End,
"timeavg" => AggregateType.TimeAverage,
"timeavg2" => AggregateType.TimeAverage2,
"interp" => AggregateType.Interpolative,
"mintime" => AggregateType.MinimumActualTime,
"maxtime" => AggregateType.MaximumActualTime,
_ => (AggregateType?)null
};
if (alias is { } a) return a;
// Every enum name (the served set; a parity test pins it), case-insensitive. Numeric strings are refused:
// Enum.TryParse would accept "6" or "99" as an undefined value.
if (!int.TryParse(trimmed, out _) &&
Enum.TryParse<AggregateType>(trimmed, ignoreCase: true, out var parsed) && Enum.IsDefined(parsed))
return parsed;
throw new ArgumentException($"Unknown aggregate: '{name}'. Supported: {SupportedAggregates}");
}
}