mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
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.
86 lines
3.2 KiB
TypeScript
86 lines
3.2 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|
||
});
|