mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
Build the composite tier of the derived-metrics layer on the shared seam: - Sleep score — a transparent 0–100 composite from sufficiency, efficiency, consistency, timing and composition sub-scores over the per-stage sleep rows, reweighting around any sub-score the data cannot support (a legacy asleep-only night drops composition gracefully rather than fabricating it). - Wellness/readiness index — extends the health-score null-redistribution blend with resting-HR, HRV (SDNN), sleep, respiratory-rate and mood deviation-from-baseline components; returns insufficient below two present components so it never headlines from one signal. - Coincident-deviation flag — counts vitals outside their personal band on the latest day and fires when two or more coincide, listing the contributing vitals as possible factors, never a cause or a diagnosis. Each composite cites its standard, carries coverage + confidence + provenance, and reweights around missing inputs. Add an FDR-controlled correlation discovery engine behind GET /api/insights/correlations: it scans a curated behaviour x outcome matrix, lag-joins each behaviour day to the next day's outcome, runs Pearson with the exact Student-t p-value, and applies Benjamini-Hochberg control across every tested pair so only statistically-defensible patterns surface — each tagged with n, r, p and the adjusted q, framed descriptive not causal. Add the score-anatomy detail view at /insights/scores/<metric>: a full score ring, ranked contributor rows each showing its push/pull and weight share, and the cited-standard provenance explainer — reused by every composite, mobile-first and accessible. Regenerate the OpenAPI contract for the new metric values + the correlation discovery response, and add the i18n keys across all six locales.
147 lines
5.0 KiB
TypeScript
147 lines
5.0 KiB
TypeScript
/**
|
|
* v1.10.0 — the route-side dispatcher: `DerivedMetricId` → its pure
|
|
* compute function → a wire-flat `Derived<unknown>`.
|
|
*
|
|
* The generic `/api/insights/derived` route validates `metric` against
|
|
* the closed registry enum, then calls `computeDerivedMetric`. Wave 1
|
|
* implements `VITALS_BASELINE` end-to-end; every other registered id is
|
|
* a metadata stub whose compute lands in W2/W3. A stub routed before its
|
|
* compute lands returns `insufficient` with `reason: "not_implemented"`
|
|
* — never a fabricated value, never a 500.
|
|
*
|
|
* Server-only — imports the server-only compute engines.
|
|
*/
|
|
import type { MeasurementType } from "@/generated/prisma/client";
|
|
import { buildInsufficient, nowProvenanceTimestamp } from "./coverage";
|
|
import {
|
|
getDerivedMetricMeta,
|
|
isVitalsBaselineType,
|
|
type DerivedMetricId,
|
|
} from "./registry";
|
|
import {
|
|
computeVitalsBaseline,
|
|
type BaselineProfile,
|
|
} from "./baseline";
|
|
import { computeFitnessAge } from "./fitness-age";
|
|
import { computeVascularAgeDelta } from "./vascular-age";
|
|
import { computeHrvBalance } from "./hrv-balance";
|
|
import { computeBmi } from "./bmi";
|
|
import { computeSleepScore } from "./sleep-score";
|
|
import { computeReadiness } from "./readiness";
|
|
import { computeCoincidentDeviation } from "./coincident-deviation";
|
|
import type { Derived } from "./types";
|
|
|
|
export interface DerivedComputeArgs {
|
|
metric: DerivedMetricId;
|
|
userId: string;
|
|
profile: BaselineProfile;
|
|
/**
|
|
* Optional sub-target for metrics that baseline a single chosen type
|
|
* (`VITALS_BASELINE`). Validated against the engine's supported set.
|
|
*/
|
|
type?: string | null;
|
|
/** Trailing window override (days). */
|
|
windowDays?: number;
|
|
now?: Date;
|
|
}
|
|
|
|
/** A `not_implemented` insufficient — the stub guard for W2/W3 metrics. */
|
|
function notImplemented(
|
|
inputs: string[],
|
|
now: Date,
|
|
): Derived<unknown> {
|
|
const computedAt = nowProvenanceTimestamp(now);
|
|
return buildInsufficient<unknown>({
|
|
coverage: { requiredInputs: 1, presentInputs: 0, historyDays: 0, missing: inputs },
|
|
provenance: { inputs, source: "none", windowDays: 0, computedAt },
|
|
reason: "not_implemented",
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Dispatch a derived-metric id to its compute function. Returns the flat
|
|
* `Derived<unknown>` the route wraps in the standard envelope. The caller
|
|
* has already Zod-validated `metric` against the registry enum, so an
|
|
* unknown id never reaches here.
|
|
*/
|
|
export async function computeDerivedMetric(
|
|
args: DerivedComputeArgs,
|
|
): Promise<Derived<unknown>> {
|
|
const now = args.now ?? new Date();
|
|
const meta = getDerivedMetricMeta(args.metric);
|
|
if (!meta) {
|
|
// Defensive — the route validates against the enum first.
|
|
return notImplemented([], now);
|
|
}
|
|
|
|
if (!meta.implemented) {
|
|
return notImplemented(meta.inputs.map(String), now);
|
|
}
|
|
|
|
switch (args.metric) {
|
|
case "VITALS_BASELINE": {
|
|
// The baseline engine needs a single chosen vital; default to
|
|
// RESTING_HEART_RATE when the caller omits one (the catalogue's
|
|
// canonical example).
|
|
const requested = args.type ?? "RESTING_HEART_RATE";
|
|
if (!isVitalsBaselineType(requested)) {
|
|
return buildInsufficient<unknown>({
|
|
coverage: {
|
|
requiredInputs: 1,
|
|
presentInputs: 0,
|
|
historyDays: 0,
|
|
missing: [requested],
|
|
},
|
|
provenance: {
|
|
inputs: [requested],
|
|
source: "none",
|
|
windowDays: 0,
|
|
computedAt: nowProvenanceTimestamp(now),
|
|
},
|
|
reason: "unsupported_baseline_type",
|
|
});
|
|
}
|
|
return computeVitalsBaseline(args.userId, args.profile, {
|
|
type: requested as MeasurementType,
|
|
windowDays: args.windowDays,
|
|
now,
|
|
}) as Promise<Derived<unknown>>;
|
|
}
|
|
case "FITNESS_AGE":
|
|
return computeFitnessAge(args.userId, args.profile, {
|
|
windowDays: args.windowDays,
|
|
now,
|
|
}) as Promise<Derived<unknown>>;
|
|
case "VASCULAR_AGE_DELTA":
|
|
return computeVascularAgeDelta(args.userId, args.profile, {
|
|
windowDays: args.windowDays,
|
|
now,
|
|
}) as Promise<Derived<unknown>>;
|
|
case "HRV_BALANCE":
|
|
return computeHrvBalance(args.userId, args.profile, {
|
|
windowDays: args.windowDays,
|
|
now,
|
|
}) as Promise<Derived<unknown>>;
|
|
case "BMI":
|
|
return computeBmi(args.userId, args.profile, {
|
|
case "SLEEP_SCORE":
|
|
return computeSleepScore(args.userId, args.profile, {
|
|
windowDays: args.windowDays,
|
|
now,
|
|
}) as Promise<Derived<unknown>>;
|
|
case "READINESS":
|
|
return computeReadiness(args.userId, args.profile, {
|
|
windowDays: args.windowDays,
|
|
now,
|
|
}) as Promise<Derived<unknown>>;
|
|
case "COINCIDENT_DEVIATION":
|
|
return computeCoincidentDeviation(args.userId, args.profile, {
|
|
windowDays: args.windowDays,
|
|
now,
|
|
}) as Promise<Derived<unknown>>;
|
|
default:
|
|
// Registered + implemented but no dispatch arm — treat as a stub.
|
|
return notImplemented(meta.inputs.map(String), now);
|
|
}
|
|
}
|