Files
HealthLog/src/__tests__/spread-single-implementation-guard.test.ts
T
Marc-André Bombeck ac86199808 fix(stats): one standard deviation with its kind named at the call site
About a dozen hand-rolled spreads each picked population or sample
variance without saying so, and the two surfaces a person compares
picked differently: the series API reported the population SD of a
blood-pressure series, the MCP tools and the AI payload the sample SD of
the same readings, about 22 % apart at three readings.

`lib/stats/spread` holds `variance` and `standardDeviation` with an
explicit `population | sample` kind and `READING_SPREAD`, the kind every
surface uses to report the spread of a person's readings. The series
route and the feature payload both report it; the spreads that feed a
verdict (the normal-swing checks, the changepoint threshold) and the
clinical glucose SD stay the sample estimate, now said out loud. A guard
fails on a new hand-rolled spread and on the two surfaces drifting apart.
2026-10-20 04:36:54 +02:00

86 lines
3.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Structural guard: a standard deviation is computed in `lib/stats/spread`,
* with its kind named at the call site.
*
* About a dozen hand-rolled spreads each picked population (÷ n) or sample
* (÷ n − 1) without saying so, and the two surfaces a person compares picked
* differently: the series API reported the population SD of a blood-pressure
* series, the MCP and AI payload the sample SD of the same readings, about
* 22 % apart at three readings. The reading spread both report is now
* `READING_SPREAD`.
*
* The matcher flags a squared deviation from a mean in a file that also takes
* a square root. The files below compute one for a test statistic or a fit,
* never as a reported spread, and say so. The doctor report's glucose panel
* composes its SD from per-bucket sums of squares (sample, the same rule as
* `glucoseSD`) and is out of the matcher's reach by shape. Comments are
* stripped first; the planted case proves the matcher fires.
*/
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import { walkSourceFiles } from "./helpers/source-files";
const SRC = join(process.cwd(), "src");
const ALLOWED: Record<string, string> = {
"lib/stats/spread.ts": "The implementation.",
"lib/insights/correlations.ts":
"The correlation engine's centred sums and the Welch t-test variances.",
"lib/insights/mood-tag-influence.ts": "Welch t-test variances.",
"lib/insights/mood-pattern-cards.ts": "A pooled variance for a t statistic.",
"lib/insights/derived/trajectory.ts":
"The residual standard error of a regression fit (÷ n − 2).",
};
const SQUARED_DEVIATION =
/\(\s*[\w.[\]]+\s*-\s*[\w.[\]]+\s*\)\s*\*\*\s*2|\(\s*([\w.[\]]+)\s*-\s*([\w.[\]]+)\s*\)\s*\*\s*\(\s*\1\s*-\s*\2\s*\)/;
function stripComments(text: string): string {
return text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/^\s*\/\/.*$/gm, "");
}
function computesSpread(code: string): boolean {
return SQUARED_DEVIATION.test(code) && code.includes("Math.sqrt(");
}
function read(file: string): string {
return stripComments(readFileSync(join(SRC, file), "utf8"));
}
describe("one standard deviation", () => {
it("the matcher fires on a hand-rolled spread", () => {
expect(
computesSpread(
"const v = xs.reduce((s, x) => s + (x - m) ** 2, 0) / n; Math.sqrt(v);",
),
).toBe(true);
expect(
computesSpread("acc += (v - mean) * (v - mean); Math.sqrt(acc / n);"),
).toBe(true);
});
it("no file outside the list computes its own", () => {
const offenders = walkSourceFiles(SRC, { floor: 3000 })
.filter(
(file) =>
!file.startsWith("generated/") &&
!file.includes("__tests__/") &&
!/\.test\.tsx?$/.test(file),
)
.filter((file) => computesSpread(read(file)) && !(file in ALLOWED));
expect(offenders).toEqual([]);
});
it("the series API and the AI and MCP payload report the same spread", () => {
expect(read("app/api/measurements/series/route.ts")).toMatch(
/standardDeviation\(\s*values,\s*READING_SPREAD\s*\)/,
);
const features = read("lib/insights/features.ts");
expect(
features.match(/roundedSpread\(\s*vals,\s*READING_SPREAD\s*\)/g),
).toHaveLength(2);
});
});