fix(write): re-gate UInt1 — historian creates degenerate UInt1 analog tags

Live evidence: EnsureTags(UInt1) returns success but the server stores a
degenerate tag (descriptor type byte 0x00, no GUID/name), so GetTagInfo
truncates and writes fail. Not client-fixable on the analog path. Int8/UInt8
stay GREEN (live-proven). UInt1 reverts to ProtocolEvidenceMissingException
(fail-closed); golden rows removed; negative-gate tests added. Removed the
one-off capture diagnostic.

Claude-Session: https://claude.ai/code/session_012SDSQ3AcaXqPcBtDESBRii
This commit is contained in:
Joseph Doherty
2026-06-25 15:27:04 -04:00
parent aa56d2d81b
commit 95b924cdbe
4 changed files with 25 additions and 26 deletions
@@ -32,8 +32,9 @@ namespace AVEVA.Historian.Client.Wcf;
/// Int8 → Int64(8) · UInt8 → UInt64(8)
/// </code>
///
/// Live-captured for Float/Double/Int2/Int4/UInt4; UInt1/Int8/UInt8 mirror the captured
/// Double value layout (8 LE bytes for Int8/UInt8, 1 byte for UInt1) pending live round-trip.
/// Live-captured for Float/Double/Int2/Int4/UInt4; Int8/UInt8 mirror the captured
/// Double value layout (8 LE bytes) pending live round-trip. UInt1 is re-gated: the
/// historian accepts EnsureTags(UInt1) but stores a degenerate tag — writes would fail.
/// Other tag types have no captured value encoding and are rejected.
/// </remarks>
internal static class HistorianHistoricalWriteProtocol
@@ -54,7 +55,8 @@ internal static class HistorianHistoricalWriteProtocol
/// <paramref name="tagGuid"/> is the per-tag GUID (from the gRPC tag-info read),
/// <paramref name="dataType"/> is the tag's declared analog type (selects the value width), and
/// <paramref name="receivedTimeUtc"/> is the storage/received timestamp the orchestrator stamps.
/// Throws <see cref="ProtocolEvidenceMissingException"/> for tag types without a captured encoding.
/// Throws <see cref="ProtocolEvidenceMissingException"/> for tag types without a captured encoding
/// (including UInt1, which is re-gated — the historian creates a degenerate UInt1 analog tag).
/// Int8/UInt8 carry exact magnitude only up to 2^53 — the value API is <see langword="double"/>;
/// full 64-bit range is a separate follow-on.
/// </summary>
@@ -121,10 +123,6 @@ internal static class HistorianHistoricalWriteProtocol
BinaryPrimitives.WriteUInt32LittleEndian(b, checked((uint)value));
return b;
}
case HistorianDataType.UInt1:
{
return [checked((byte)value)];
}
case HistorianDataType.Int8:
{
byte[] b = new byte[8];
@@ -140,7 +138,7 @@ internal static class HistorianHistoricalWriteProtocol
default:
throw new ProtocolEvidenceMissingException(
$"AddHistoricalValuesAsync has no captured value encoding for tag data type '{dataType}'. " +
"Captured types: Float, Double, Int2, Int4, UInt4, UInt1, Int8, UInt8.");
"Captured types: Float, Double, Int2, Int4, UInt4, Int8, UInt8.");
}
}