Speculative-items sweep: IntegralDivisor, cert tests, D3/D1/D2 findings

Plan: docs/plans/speculative-items-sweep.md (also covers parallelism +
findings).

Implemented:
- C3: HistorianTagDefinition.IntegralDivisor (default 1.0). Wire bytes
  flip per the captured native serializer; live probe shows the server
  stores IntegralDivisor on EngineeringUnit (shared) rather than per-tag,
  so the value is accepted on the wire but doesn't visibly persist for
  the test EU. Documented in the property's doc-comment.
- E: HistorianWcfCertOptionTests (5 tests) covering AllowUntrustedServer-
  Certificate validator installation + ServerDnsIdentity propagation
  through CreateEndpointAddress and CreateBindingPair.

Investigated + documented (deferred):
- D3: Discrete/String/Int1/Int8/UInt8 EnsT2 root cause — server-side
  ValidationFailed: "Transaction validation failed". Native AddTag's
  validator rejects non-analog types; not a wire-format issue. To unlock,
  need to capture a working native flow via a different code path
  (likely SMC's tag-import path or AddTagExtendedProperties carrying
  type-specific metadata). Defer until a customer asks.
- D1: AddTagExtendedProperties feasibility — managed surface confirmed
  (ArchestrA.HistorianAccess.AddTagExtendedProperties + WCF op
  AddTagExtendedPropertyGroups). Cost estimated at 1-2 days of focused
  RE work due to CTagExtendedPropertyGroup payload complexity. Defer.
- D2: AddRevisionValuesBegin/Value/End — sub-plan written at
  docs/plans/revision-write-path.md with 5-step capture sequence,
  workstream estimates, and risk register. Implementation deferred.

177/177 tests pass (was 172; +5 cert tests + 1 IntegralDivisor unit
test, harness probe results not committed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-05 00:11:40 -04:00
parent 549995e4a9
commit f4709ff143
7 changed files with 420 additions and 3 deletions
@@ -69,4 +69,16 @@ public sealed record HistorianTagDefinition
/// (Cyclic = 1, Delta = 2).
/// </summary>
public HistorianStorageType StorageType { get; init; } = HistorianStorageType.Cyclic;
/// <summary>
/// Divisor applied when storing integral values for trend integration. Default 1.0.
/// Wire bytes flip correctly per the captured native serializer, but live testing
/// 2026-05-05 showed the server stores <c>IntegralDivisor</c> on
/// <c>EngineeringUnit</c> (shared across all tags using that EU) rather than
/// per-tag — so a non-default value sent here is accepted on the wire but does
/// not visibly persist in <c>EngineeringUnit.IntegralDivisor</c> for the test
/// EU. Exposed for completeness and forward-compatibility; check your server's
/// behavior before relying on it.
/// </summary>
public double IntegralDivisor { get; init; } = 1.0;
}
@@ -145,7 +145,8 @@ internal static class HistorianTagWriteProtocol
double maxRaw = DefaultMaxRaw,
uint storageRateMs = DefaultStorageRateMs,
bool applyScaling = false,
Models.HistorianStorageType storageType = Models.HistorianStorageType.Cyclic)
Models.HistorianStorageType storageType = Models.HistorianStorageType.Cyclic,
double integralDivisor = 1.0)
{
if (storageRateMs == 0)
{
@@ -188,7 +189,7 @@ internal static class HistorianTagWriteProtocol
WriteCompactAscii(w, engineeringUnit ?? string.Empty); // var
w.Write(IntegralDivisorMagic); // uint32 (purpose unclear — captured constant)
w.Write(1.0); // double
w.Write(integralDivisor); // double IntegralDivisor (default 1.0)
w.Write(applyScaling ? AnalogTrailerScalingEnabled : AnalogTrailerScalingDisabled);
return ms.ToArray();
@@ -110,7 +110,8 @@ internal sealed class HistorianWcfTagWriteOrchestrator
maxRaw: definition.MaxRaw,
storageRateMs: definition.StorageRateMs,
applyScaling: definition.ApplyScaling,
storageType: definition.StorageType);
storageType: definition.StorageType,
integralDivisor: definition.IntegralDivisor);
bool ok = historyChannel.EnsureTags2(
handle: handle,