When the data starts inside an interval (no start bound) and its first value is
Uncertain, the frozen 1.5.378.182 AverageAggregateCalculator integrated the
uncovered span as a ramp from 0 and divided TimeAverage by the whole interval:
GetRegionsInValueSet (ignoreBadData, sloped) overwrites the BadNoData start
region's status with UncertainDataSubNormal in its one branch without a not-Bad
guard. Live AggregateFilter therefore disagreed with HistoryRead and Part 13
§5.4.3.6/§5.4.3.8 ("No Start Bound: Calculate Partial Interval"); QP: Total
188.96 vs 163.96, TimeAverage 9.448 vs 10.931. SDK 2.0.1 stock does the same.
Owner-approved 2026-11-11.
- New app-owned CoveredTimeAverageAggregateCalculator subclasses the baseline
and recomputes only TimeAverage/Total, restoring the Bad status of a region
that starts at a Bad point (only the start bound can). Only such an interval's
value changes; status, timestamps, type and bits are the baseline's. Stepped
and TreatUncertainAsBad=true never reached the defect. CompatibilityAggregate-
Factory routes exactly those two IDs; copied SDK files untouched.
- Golden test: 0 fixture vectors change (every data set has a usable value at
-10 s), asserted. New sweep over late-start variants of every data set (cut
+5 s / +25 s, first non-Bad sample Uncertain, all directions/stepping/configs/
incremental): 150 / 142 of 384 change, each pinned to a baseline-derived
oracle (untouched baseline's value with that sample Good, its status/bits on
the variant itself) - never our calculator. Plus a hand-computed case.
- Live-vs-HistoryRead parity (TimeWeightedAggregator) forward + reverse, no
masks, with discrimination against the pre-#821 baseline; QualityMix now
compares QP's TimeAverage/Total with the live oracle on both SQL backends;
booted-runtime facts test expects 163.96 / 10.93.
- README recorded decision, ServerAggregateConfiguration remarks, oracle
remarks, docs/Historian.md and .claude/rules/historian.md updated.
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
The frozen 1.5.378.182 StatusAggregateCalculator counts Uncertain regions as
Good under TreatUncertainAsBad=false, so live AggregateFilter PercentGood /
DurationGood disagreed with HistoryRead (Part 13 §5.4.3.31/§5.4.3.33, Mantis
0006166) whenever Uncertain data was present. Owner-approved 2026-11-11.
- New app-owned GoodOnlyStatusAggregateCalculator subclasses the baseline and
replaces only the PercentGood/DurationGood computation (Good regions only);
CompatibilityAggregateFactory routes exactly those two IDs to it. The copied
SDK files are untouched; the other 35 aggregates are byte-identical.
- Golden test: the 64 changed vectors (38 PercentGood + 26 DurationGood) are
exempted exactly — those whose baseline output differs from the same case's
baseline TreatUncertainAsBad=true twin — and pinned to that twin (baseline
evidence, not regenerated from the implementation); plus a full Good-only
pin over all 352 PercentGood/DurationGood vectors and a hand-computed case.
- Live-vs-history parity over Good/Uncertain/Bad mixes (no masks), with a
discrimination check against the pre-#809 baseline; the booted-runtime facts
test now expects 8000 ms / 40 %.
- SdkAggregateOracle: Run/RunLive = the registered factory; RunSdkAsIs = the
untouched baseline; its Part13GoodCalculator duplicate is removed.
- README (recorded decision), ServerAggregateConfiguration remarks,
docs/Historian.md and .claude/rules/historian.md updated.
Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
#820: SqlServer + QuestDB now serve Part 13 DurationGood / DurationBad from the
same Good / Bad region totals TimeWeightedAggregator already computes for
PercentGood / PercentBad — reported in milliseconds (OPC UA Duration, the SDK's
unit), Uncertain time in neither (Mantis 0006166), same BadNoData / status /
Partial rules. AggregateFamilies.Served grows 17 -> 19, so i=11201 and the
up-front refusal follow automatically; Gateway still refuses both (explicit
served set, unchanged). Client.Shared AggregateType / mapper / CLI gain both.
The #796 oracle answers DurationGood through the Good-only subclass as well.
QualityMix conformance asserts both directly (Q and QP, hand values, ms, and
= PercentGood x interval), replaces the "refused up front" test, and adds
Duration* to the forward/reverse/truncated oracle comparisons.
#821 (facts only, no decision): SdkAggregateParityTests pins an Uncertain-first
late-start interval (QP over [0,20), both directions) against ours (Part 13
Total 163.96, TimeAverage 10.93), the frozen 1.5.378.182 calculators (188.96,
9.448), SDK 2.0.1's stock calculators (188.96, 9.448) and a Good-first control
(all 163.96); LiveAggregateLateStartFactsTests pins the same through the booted
server's runtime AggregateManager (live AggregateFilter) incl. DurationGood
15000 / PercentGood 75 there vs 8000 / 40 in HistoryRead (#809). QualityMix
adds the reverse late-start variant (reverse TW pin + reverse oracle windows).
Co-Authored-By: Claude <noreply@anthropic.com>
QualityMixConformance (SqlServer + QuestDB): tag Q mixes Good/Uncertain/Bad
regions across eight 10 s intervals (two all-Uncertain - Mantis 0006166 0%/0% -
one through an Uncertain bound, plus Uncertain bounds from a sloped non-Good end
and from a hold into an outage); tag QP has data starting and ending inside
intervals. PercentGood/PercentBad are asserted against hand-derived Part 13
DurationGood/DurationBad, the oracle is checked against the same hand values,
and Count/Average/TimeAverage(2)/Total(2)/Start/End/Delta/Interpolative are
compared with the #796 oracle forward, reverse and truncated.
Teeth: with SdkAggregateOracle.Run answering PercentGood from the baseline
(Uncertain as Good), 8 tests fail per backend; all pass with the fixed oracle.
Records a new SDK divergence: with an Uncertain first sample the baseline
TimeAverage/Total integrate the pre-data span as a ramp from 0; ours covers
only the data (Part 13) - pinned by hand.