Files
HealthLog/src/lib/analytics/score/index.ts
T
Marc-André Bombeck 5d7d00949c Resolve the configured flag on the score composite
One boolean on the composite: true when the account's resolved
composition differs from the composition its defaults would resolve to
today, both narrowed by the same modules. Decided once in the score
reader, from the same config and module map the composition came from,
so no client interprets a config blob to answer it.

This is not hasSelection. An account that opened the surface and kept
every pillar has written a selection and is not configured; an account
whose modules alone narrow the set has authored nothing and is not
configured either.
2026-07-31 18:02:22 +02:00

106 lines
3.2 KiB
TypeScript

import type { Derived } from "@/lib/insights/derived/types";
import { computeActivityPillar } from "./activity";
import { computeAdiposityPillar } from "./adiposity";
import { computeBloodPressurePillar } from "./blood-pressure";
import { computeComposite } from "./composite";
import { computeFitnessPillar } from "./fitness";
import { computeGlycaemiaPillar } from "./glycaemia";
import { computeLipidsPillar } from "./lipids";
import { computeSleepPillar } from "./sleep";
import {
SCORE_PILLAR_DOMAINS,
SCORE_PILLAR_IDS,
SCORE_VERSION,
type CompositeValue,
type HealthScoreReport,
type PillarInputs,
type ScorePillarId,
type ScorePillarResult,
type WeightGoalValue,
} from "./types";
import { computeWellbeingPillar } from "./wellbeing";
export interface ComputeHealthScoreInput {
asOf: Date;
availablePillars: ScorePillarId[];
/**
* Whether `availablePillars` is an authored recipe rather than the
* account's defaults, resolved by `resolveScoreConfigured`. Rides to
* the composite unchanged; nothing here re-derives it.
*/
configured: boolean;
pillars: PillarInputs;
weightGoal: Derived<WeightGoalValue>;
delta?: number | null;
deltaReason?: HealthScoreReport["deltaReason"];
}
export function computeHealthScore(
input: ComputeHealthScoreInput,
): HealthScoreReport {
const byId: Record<ScorePillarId, ScorePillarResult> = {
BLOOD_PRESSURE: {
id: "BLOOD_PRESSURE",
domain: SCORE_PILLAR_DOMAINS.BLOOD_PRESSURE,
result: computeBloodPressurePillar(input.pillars.BLOOD_PRESSURE),
},
GLYCAEMIA: {
id: "GLYCAEMIA",
domain: SCORE_PILLAR_DOMAINS.GLYCAEMIA,
result: computeGlycaemiaPillar(input.pillars.GLYCAEMIA),
},
ACTIVITY: {
id: "ACTIVITY",
domain: SCORE_PILLAR_DOMAINS.ACTIVITY,
result: computeActivityPillar(input.pillars.ACTIVITY),
},
SLEEP: {
id: "SLEEP",
domain: SCORE_PILLAR_DOMAINS.SLEEP,
result: computeSleepPillar(input.pillars.SLEEP),
},
ADIPOSITY: {
id: "ADIPOSITY",
domain: SCORE_PILLAR_DOMAINS.ADIPOSITY,
result: computeAdiposityPillar(input.pillars.ADIPOSITY),
},
WELLBEING: {
id: "WELLBEING",
domain: SCORE_PILLAR_DOMAINS.WELLBEING,
result: computeWellbeingPillar(input.pillars.WELLBEING),
},
FITNESS: {
id: "FITNESS",
domain: SCORE_PILLAR_DOMAINS.FITNESS,
result: computeFitnessPillar(input.pillars.FITNESS),
},
LIPIDS: {
id: "LIPIDS",
domain: SCORE_PILLAR_DOMAINS.LIPIDS,
result: computeLipidsPillar(input.pillars.LIPIDS),
},
};
const available = new Set(input.availablePillars);
const pillars = SCORE_PILLAR_IDS.filter((id) => available.has(id)).map(
(id) => byId[id],
);
return {
composite: computeComposite({
pillars,
availablePillars: input.availablePillars,
asOf: input.asOf,
configured: input.configured,
}),
pillars,
delta: input.delta ?? null,
deltaReason: input.deltaReason ?? "no_previous_window",
scoreVersion: SCORE_VERSION,
weightGoal: input.weightGoal,
algorithmNotice: null,
};
}
export type { CompositeValue, HealthScoreReport };
export * from "./types";