test(rollups): assert regression parity relatively, not at absolute 9 dp

The DST-boundary parity case seeds two readings on the same UTC day, so the
windowed compose folds a closed form over a large epoch-day x (~20540). The
n*Sxx - Sx^2 denominator subtracts two ~1e10 magnitudes and loses about ten
significant digits to cancellation before the divide, so an absolute
toBeCloseTo at 9 dp is the wrong gauge for a slope that small. The populator
sums per-raw-reading accumulators grouped by day, so both paths regress over
the identical raw-row x set and agree mathematically; the residual is float
summation order alone. Switch all parity assertions to a shared relative
tolerance (1e-9 of scale), far tighter than the 2-3 dp the read tier rounds to.
This commit is contained in:
Marc-André Bombeck committed 2026-06-22 09:13:02 +02:00
1 parent 755f7e101b
commit ed96a8a5d1
1 file changed
+38 -11
@@ -14,7 +14,20 @@
* diverge), folds the rollups, then asserts:
*
* 1. `composeRegression` over the folded DAY-bucket accumulators equals
* `REGR_SLOPE` / `REGR_R2` / `STDDEV_POP` over the raw rows to ≤ 1e-9.
* `REGR_SLOPE` / `REGR_R2` / `STDDEV_POP` over the raw rows to a tight
* relative tolerance.
*
* Tolerance note: both paths fold the SAME closed form over the SAME raw-row
* epoch-day x set, so they agree mathematically. They disagree only in float
* summation order — Postgres's parallel aggregate accumulators vs. the
* textbook `n·Σxy − Σx·Σy` closed form `composeRegression` evaluates from the
* stored sums. With a UTC epoch-day x-axis (x ≈ 20 540), `n·Σxx − Σx²` is a
* difference of two ~1e10 magnitudes, so the closed form sheds ~10 significant
* digits to cancellation before the divide. That makes an ABSOLUTE 9-dp
* `toBeCloseTo` the wrong gauge for a slope this small — the residual is float
* reorder noise, not a data divergence. Parity is asserted RELATIVELY at
* 1e-9, which is far tighter than the 2–3 dp the read tier ultimately rounds
* the windowed slope / r² / sd to.
* 2. The source-collapse runs BEFORE accumulator summation — a dual-source
* day contributes only the canonical source's accumulators, matching a
* live regression over the canonical-source rows.
@@ -38,6 +51,17 @@ vi.mock("@/lib/jobs/boss-instance", () => ({
getGlobalBoss: vi.fn(() => null),
}));
/**
* Relative parity assertion for two values that are mathematically equal but
* differ only by float summation order (see the tolerance note in the file
* header). Absolute `toBeCloseTo` is the wrong gauge across magnitudes; this
* pins |a − b| ≤ rel · max(|a|, |b|, 1) with a tight 1e-9 relative bound.
*/
function expectParity(composed: number, live: number, rel = 1e-9): void {
const scale = Math.max(Math.abs(composed), Math.abs(live), 1);
expect(Math.abs(composed - live)).toBeLessThanOrEqual(rel * scale);
}
/** Live REGR row shape (one per type over the seeded window). */
interface LiveRegrRow {
slope: number | null;
@@ -186,10 +210,10 @@ describe("rollup regression accumulators — live parity (v1.20.0 F6)", () => {
expect(live.slope).not.toBeNull();
expect(composed.slope).not.toBeNull();
// Bit-identical parity — the closed form is the same on both paths.
expect(composed.slope!).toBeCloseTo(live.slope!, 9);
expect(composed.r2!).toBeCloseTo(live.r2!, 9);
expect(composed.sdPop!).toBeCloseTo(live.sd_pop!, 9);
// Same closed form on both paths — parity holds to float reorder noise.
expectParity(composed.slope!, live.slope!);
expectParity(composed.r2!, live.r2!);
expectParity(composed.sdPop!, live.sd_pop!);
});
it("source-collapse precedes summation: composed RHR equals live REGR over the WHOOP source only", async () => {
@@ -250,9 +274,9 @@ describe("rollup regression accumulators — live parity (v1.20.0 F6)", () => {
const composed = composeRegression(acc);
expect(Number(live.n)).toBe(days.length);
expect(composed.slope!).toBeCloseTo(live.slope!, 9);
expect(composed.r2!).toBeCloseTo(live.r2!, 9);
expect(composed.sdPop!).toBeCloseTo(live.sd_pop!, 9);
expectParity(composed.slope!, live.slope!);
expectParity(composed.r2!, live.r2!);
expectParity(composed.sdPop!, live.sd_pop!);
// The blend would be wrong: a regression over BOTH sources differs from
// the WHOOP-only one, proving the collapse-before-sum matters.
@@ -296,8 +320,11 @@ describe("rollup regression accumulators — live parity (v1.20.0 F6)", () => {
const composed = composeRegression(acc);
expect(Number(live.n)).toBe(seed.length);
expect(composed.slope!).toBeCloseTo(live.slope!, 9);
expect(composed.r2!).toBeCloseTo(live.r2!, 9);
expect(composed.sdPop!).toBeCloseTo(live.sd_pop!, 9);
// The two same-UTC-day DST readings (00:30Z + 02:30Z) make this the
// multi-reading-per-day case where the large epoch-day x drives the worst
// closed-form cancellation — relative parity is the honest gauge.
expectParity(composed.slope!, live.slope!);
expectParity(composed.r2!, live.r2!);
expectParity(composed.sdPop!, live.sd_pop!);
});
});