Files
memind/health-observation-validate.mjs
T
john f96fdcb3b9 feat(health): add P0 health channel kernel and encrypted health zone.
Lock health recording behind confirm-only state machines, reject public publishes for the health category, and ship the 14-day experiment Page Data templates with tests.

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

76 lines
2.7 KiB
JavaScript

const RANGES = Object.freeze({
bp_systolic: { min: 60, max: 260, unit: 'mmHg' },
bp_diastolic: { min: 40, max: 160, unit: 'mmHg' },
hr: { min: 30, max: 200, unit: 'bpm' },
spo2: { min: 50, max: 100, unit: '%' },
temperature: { min: 33, max: 43, unit: 'celsius' },
weight: { min: 20, max: 250, unit: 'kg' },
sleep_minutes: { min: 0, max: 16 * 60, unit: 'min' },
});
export const HEALTH_CONTEXTS = Object.freeze(['morning', 'evening', 'other']);
export function inferBloodPressureContext(observedAt, { now = new Date(), timeZone = 'Asia/Shanghai' } = {}) {
const date = observedAt instanceof Date ? observedAt : new Date(observedAt ?? now);
if (Number.isNaN(date.getTime())) return 'other';
const hourText = new Intl.DateTimeFormat('en-GB', {
hour: '2-digit',
hourCycle: 'h23',
timeZone,
}).format(date);
const hour = Number(hourText);
if (hour < 10) return 'morning';
if (hour >= 18) return 'evening';
return 'other';
}
export function validateMetricValue(metricType, value) {
const range = RANGES[metricType];
if (!range) {
return { ok: false, error: 'unknown_metric' };
}
const num = Number(value);
if (!Number.isFinite(num)) {
return { ok: false, error: 'not_numeric' };
}
if (num < range.min || num > range.max) {
return { ok: false, error: 'out_of_range', range };
}
return { ok: true, value: num, unit: range.unit };
}
export function validateBloodPressurePair(systolic, diastolic) {
const sys = validateMetricValue('bp_systolic', systolic);
const dia = validateMetricValue('bp_diastolic', diastolic);
if (!sys.ok) return { ok: false, field: 'bp_systolic', ...sys };
if (!dia.ok) return { ok: false, field: 'bp_diastolic', ...dia };
if (sys.value <= dia.value) {
return { ok: false, error: 'systolic_not_greater', field: 'pair' };
}
return { ok: true, systolic: sys.value, diastolic: dia.value };
}
export function markSuspectVsBaseline(value, baselineMean, { ratio = 0.5 } = {}) {
const mean = Number(baselineMean);
const num = Number(value);
if (!Number.isFinite(mean) || mean <= 0 || !Number.isFinite(num)) {
return { qualityFlag: 'ok', suspect: false };
}
const deviation = Math.abs(num - mean) / mean;
if (deviation >= ratio) {
return { qualityFlag: 'suspect', suspect: true, deviation };
}
return { qualityFlag: 'ok', suspect: false, deviation };
}
export function resolveQualityAfterConfirm({ suspect = false, userCorrected = false } = {}) {
if (userCorrected) return 'user_corrected';
if (suspect) return 'ok';
return 'ok';
}
export function observationDedupKey({ metricType, observedAt, sourceRef } = {}) {
if (!sourceRef) return null;
return `${metricType}|${observedAt}|${sourceRef}`;
}