Files
memind/health-baseline-engine.mjs
T
john 2baf29b3ae feat(health): complete P0 health channel — baseline engine, page-data, MindSpace UI
Deliver encrypted health zone, observation API, baseline maturity pipeline,
page-data bindings, and H5/WeChat channel integration for health P0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 18:04:57 +08:00

85 lines
2.7 KiB
JavaScript

import {
isValidObservationForBaseline,
resolveBaselineMaturity,
} from './health-baseline-maturity.mjs';
const WINDOWS = Object.freeze([7, 30, 90]);
function withinWindow(observedAt, windowDays, now = Date.now()) {
const ts = Number(observedAt);
if (!Number.isFinite(ts)) return false;
const start = now - windowDays * 24 * 60 * 60 * 1000;
return ts >= start && ts <= now;
}
function mean(values) {
if (!values.length) return null;
return values.reduce((sum, v) => sum + v, 0) / values.length;
}
function stddev(values, avg) {
if (values.length < 2) return 0;
const m = avg ?? mean(values);
const variance = values.reduce((sum, v) => sum + (v - m) ** 2, 0) / values.length;
return Math.sqrt(variance);
}
export function computeMetricBaseline(
observations = [],
{
metricType,
context = 'any',
windowDays = 30,
now = Date.now(),
} = {},
) {
const rows = observations.filter((row) => {
if (!isValidObservationForBaseline(row)) return false;
if (row.metricType !== metricType) return false;
if (row.valueNum == null) return false;
if (context !== 'any' && row.context !== context) return false;
return withinWindow(row.observedAt, windowDays, now);
});
const values = rows.map((row) => Number(row.valueNum)).filter(Number.isFinite);
const sampleCount = values.length;
const avg = mean(values);
return {
metricType,
context,
windowDays,
sampleCount,
maturity: resolveBaselineMaturity(sampleCount),
mean: avg == null ? null : Number(avg.toFixed(2)),
stddev: avg == null ? null : Number(stddev(values, avg).toFixed(2)),
min: values.length ? Math.min(...values) : null,
max: values.length ? Math.max(...values) : null,
};
}
export function computeHealthBaselines(observations = [], { now = Date.now() } = {}) {
const metricContexts = [
{ metricType: 'bp_systolic', context: 'morning' },
{ metricType: 'bp_diastolic', context: 'morning' },
{ metricType: 'bp_systolic', context: 'evening' },
{ metricType: 'bp_diastolic', context: 'evening' },
{ metricType: 'hr', context: 'any' },
{ metricType: 'weight', context: 'any' },
{ metricType: 'spo2', context: 'any' },
{ metricType: 'sleep_minutes', context: 'any' },
];
const baselines = [];
for (const windowDays of WINDOWS) {
for (const spec of metricContexts) {
baselines.push(computeMetricBaseline(observations, { ...spec, windowDays, now }));
}
}
return baselines;
}
export function deviationFromBaseline(value, baselineMean) {
const num = Number(value);
const mean = Number(baselineMean);
if (!Number.isFinite(num) || !Number.isFinite(mean) || mean === 0) return null;
return Number((((num - mean) / mean) * 100).toFixed(1));
}