feat(mtconnect): authoring DataType warning + loud ignored-DeviceName warning (#613 slice)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 06:51:40 -04:00
parent 3baa332d6b
commit b78c7ff9db
6 changed files with 162 additions and 2 deletions
@@ -80,6 +80,10 @@
{
<div class="text-danger small mt-2">@error</div>
}
@foreach (var warning in _m.Warnings())
{
<div class="text-warning small mt-2">@warning</div>
}
@code {
/// <summary>The tag's raw TagConfig JSON (supports <c>@bind-ConfigJson</c>).</summary>
@@ -216,6 +216,55 @@ public sealed class MTConnectTagConfigModel
"MTConnect");
}
/// <summary>
/// Advisory warnings (#613) — NEVER an error and never routed through <see cref="Validate"/>: the
/// deploy gate does not refuse these, so authoring must not either ("never stricter than the gate").
/// One rule: the declared <see cref="DataType"/> cannot hold the type
/// <see cref="MTConnectDataTypeInference.Infer"/> derives from the stamped
/// <see cref="MtCategory"/>/<see cref="MtType"/>.
/// </summary>
/// <remarks>
/// A no-op unless the probe metadata was stamped (a browse-commit does; hand-authored blobs do not),
/// since there is nothing to infer from otherwise. <c>representation</c> is not on the model so it is
/// passed as null; it can only DEMOTE an item to String (DATA_SET/TABLE), which cannot create a false
/// warning because a numeric declaration on a String-inferred item is warned either way, and a SAMPLE
/// inferred Float64 is only warned about for a non-numeric, non-String declaration or an integer one.
/// TIME_SERIES arrays are not detectable without it and are not covered.
/// </remarks>
/// <returns>Zero or more human-readable warnings.</returns>
public IReadOnlyList<string> Warnings()
{
if (string.IsNullOrWhiteSpace(MtCategory)) { return []; }
var inferred = MTConnectDataTypeInference.Infer(MtCategory, MtType, Units).DataType;
var declared = DataType;
// String can hold any observation's text, so declaring it is never lossy.
if (declared == inferred || declared == DriverDataType.String) { return []; }
if (IsNumeric(inferred) && IsNumeric(declared))
{
// Float64 inferred (a SAMPLE): an integer type cannot hold a fractional reading. A wider-or-equal
// numeric holds an Int64-inferred item (PartCount, Line).
return IsIntegral(declared) && !IsIntegral(inferred)
? [$"Data type {declared} cannot hold fractional values; the Agent's /probe metadata "
+ $"({MtCategory}/{MtType}) infers {inferred}. Fractional readings will surface as a bad status."]
: [];
}
return
[
$"Data type {declared} does not match the type inferred from the Agent's /probe metadata "
+ $"({MtCategory}/{MtType} -> {inferred}). Observations that do not parse as {declared} will surface "
+ "as BadTypeMismatch/BadOutOfRange at runtime. Saving is still allowed."
];
}
private static bool IsNumeric(DriverDataType t) => t is not (DriverDataType.Boolean or DriverDataType.String
or DriverDataType.DateTime or DriverDataType.Reference);
private static bool IsIntegral(DriverDataType t) => IsNumeric(t) && t is not (DriverDataType.Float32 or DriverDataType.Float64);
// The as-loaded coercion-type TOKEN as text (see the _rawDataType and DataTypeKeys remarks), read in
// the RUNTIME's precedence so this model judges — and displays — the token the driver would actually
// read. Per key, mirroring FirstNonBlank(blob.DriverDataType, blob.DataType) over a DTO whose two