fix(historian): #680 review — recovery/only-sample TA2/Total2 value, outage-last + Boolean parity, docs

TimeAverage2/Total2: the interval the data ends inside with no non-Bad time
before its last sample (comms-loss recovery, a single sample) answers that
sample's value (Total2 0), Uncertain + Partial, instead of Bad/BadNoData —
as the SDK. Parity datasets RecoveryEnds, OutageLast, BoolSwitch with
documented masks; ProcessedConformance's reverse TA/Total mask narrowed to
the later-edge-sample interval. Docs: report-by-exception consequence,
outage-last and Boolean differences; CLAUDE.md historian bullet.
This commit is contained in:
Joseph Doherty
2026-10-11 16:02:22 -04:00
parent b61975f7d7
commit 0d206a0884
6 changed files with 218 additions and 16 deletions
@@ -352,16 +352,28 @@ public abstract class ProcessedConformance : ConformanceBase
}
}
var reverseInterpolative = agg == HistoryAggregateType.Interpolative && from > to;
// Documented SDK divergence (#680, see SdkAggregateParityTests): on a reverse read the SDK files a sample exactly
// on an interval's later edge as its early bound, so TimeAverage/Total there take the status of the segment
// beyond it (Dense: +20 next to the Bad +22.5).
var reverseEdgeStatus = from > to && agg is HistoryAggregateType.TimeAverage or HistoryAggregateType.Total;
SdkAggregateOracle.ShouldMatch(ours, SdkAggregateOracle.Run(agg, start, end, interval, rows),
var sdk = SdkAggregateOracle.Run(agg, start, end, interval, rows);
if (from > to && agg is HistoryAggregateType.TimeAverage or HistoryAggregateType.Total)
{
// Documented SDK divergence (#680, see SdkAggregateParityTests.MaskDocumented): on a reverse read the SDK files
// a sample exactly on an interval's later edge as its early bound, so a TimeAverage/Total interval ending on a
// sample takes the status of the segment beyond it (Dense: +20 next to the Bad +22.5). Only that interval,
// only Good (ours) vs Uncertain (SDK), is aligned.
var plan = BucketPlan.Create(start, end, interval);
for (long i = 0; i < plan.BucketCount; i++)
{
var k = (int)(plan.BucketCount - 1 - i);
if (k < sdk.Count && rows.Any(r => r.Item1.Ticks == plan.UpperTicks(i))
&& (ours[k].StatusCode & 0xFFFF0000u) == 0
&& (sdk[k].StatusCode & 0xFFFF0000u) == 0x40A40000u)
ours[k] = ours[k] with { StatusCode = ours[k].StatusCode | 0x40A40000u };
}
}
SdkAggregateOracle.ShouldMatch(ours, sdk,
$"{agg} {from}→{to}/{intervalS}s",
compareCalculatedInterpolatedBits: tw && !reverseInterpolative,
// pre-existing, #680: Start/End raw-sample timestamp + status; Delta status (see SdkAggregateParityTests).
compareCodeBits: agg != HistoryAggregateType.Delta && !reverseInterpolative && !startOrEnd
&& !reverseEdgeStatus,
compareCodeBits: agg != HistoryAggregateType.Delta && !reverseInterpolative && !startOrEnd,
compareTimestamps: !startOrEnd);
}
}