feat(insights): add a cited reference-range backbone and richer metric explainers

Introduce src/lib/reference-ranges.ts: a structured, citation-backed
per-metric reference-range backbone (band + source body + year) with a
deterministic four-state placement helper, read by the UI surfaces. Each
band traces to the medical-citations registry, which gains the supporting
sources (ACC/AHA 2017 and ESC 2024 blood pressure, StatPearls pulse
pressure and mean arterial pressure, ESC/ESH 2018 pulse-wave velocity,
FDA 2024 and StatPearls pulse oximetry, J Gen Intern Med 2019 temperature,
RCP NEWS2 and American Lung Association respiratory rate, visceral-fat
imaging literature, AHA 2024 resting heart rate, ESC/NASPE 1996 HRV,
Wilcox 2000, ACOG 651, Phillips 2017 and CDC 2024 sleep).

Blood pressure keeps ESH 2023 as the labelled default; ACC/AHA 2017 and
ESC 2024 appear only as displayed placement context. Enrich the metric
explainers with concise cited reference bands and a general-guidance
tooltip on every metric page: a darker-skin pulse-oximeter over-read
caveat, device-proxy honesty for readiness/recovery/strain/stress, an
HRV day-to-day-noise note, and a resting-heart-rate clarification. Fill
the empty registry normal ranges, reconcile the steps green floor to
8,000, and tighten the body-temperature band below the fever line. Add
free pulse-pressure and mean-arterial-pressure context on the blood-
pressure detail page, plus cycle-physiology and sleep-hygiene copy across
all six locales. Cycle thresholds carry a verification marker pending an
ACOG source check.
This commit is contained in:
Marc-André Bombeck committed 2026-06-18 20:17:04 +02:00
1 parent 1d83e10764
commit 9be30897fa
12 files changed
+985 -29

No files matched your search

+26
View File
@@ -170,6 +170,32 @@ export default function InsightsBlutdruckPage() {
average). */}
<MetricTargetSummary slug="blood-pressure" />
{/* v1.18.6 — free derived BP context: pulse pressure (SBP − DBP) and
mean arterial pressure ((SBP + 2·DBP)/3), computed from the most
recent reading. Pure derivation of stored BP — no schema, no extra
fetch. Calm, cited, non-alarming. */}
{(() => {
const sbp = sysSummary?.latest ?? null;
const dbp = diaSummary?.latest ?? null;
if (sbp == null || dbp == null || sbp <= dbp) return null;
const pp = Math.round(sbp - dbp);
const map = Math.round((sbp + 2 * dbp) / 3);
return (
<p
data-slot="bp-derived-caption"
className="text-muted-foreground text-sm leading-relaxed"
>
{t("insights.subPage.bpDerived.pulsePressure", {
value: pp,
})}{" "}
{t("insights.subPage.bpDerived.meanArterialPressure", {
value: map,
})}{" "}
{t("insights.subPage.bpDerived.caveat")}
</p>
);
})()}
{/* v1.12.4 — Einschätzung is the last content block; the stat strip
(rendered by the shell) closes the spine below it. */}
<SlugInsightStatusCard
+34 -1
View File
@@ -3,10 +3,16 @@
import { useEffect, useRef, type ReactNode } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { ListOrdered } from "lucide-react";
import { Info, ListOrdered } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { CoachLaunchButton } from "@/components/insights/coach-launch-button";
import { TargetAdjustButton } from "@/components/insights/target-adjust-button";
import { TargetAdjustProvider } from "@/lib/insights/target-adjust-context";
@@ -291,6 +297,33 @@ export function SubPageShell({
className="text-muted-foreground text-sm leading-relaxed"
>
{t(`insights.subPage.explainer.${explainerMetric}Body`)}
{/* v1.18.6 — a discreet general-guidance tooltip travels with
every metric definition. The reference bands in the body
above are population anchors, not medical advice; this
caption pins that framing without crowding the prose. */}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
data-slot="metric-reference-guidance"
className={cn(
"text-muted-foreground hover:text-foreground ml-1.5 inline-flex",
"align-middle focus-visible:ring-ring/50 rounded-full",
"focus-visible:ring-2 focus-visible:outline-none",
)}
aria-label={t(
"insights.subPage.explainer.referenceGuidance",
)}
>
<Info className="size-3.5" aria-hidden="true" />
</button>
</TooltipTrigger>
<TooltipContent className="max-w-xs text-pretty">
{t("insights.subPage.explainer.referenceGuidance")}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</p>
) : null}
{description && !explainerMetric ? (
@@ -0,0 +1,98 @@
import { describe, it, expect } from "vitest";
import { CITATIONS } from "../medical-citations";
import {
REFERENCE_RANGES,
REFERENCE_METRICS,
classifyReference,
getReferenceRange,
isReferenceMetric,
referenceCitation,
referenceLabel,
type ReferenceMetric,
} from "../reference-ranges";
describe("reference-ranges backbone", () => {
it("every band + conflict + headline resolves a real citation", () => {
for (const metric of REFERENCE_METRICS) {
const range = REFERENCE_RANGES[metric];
expect(CITATIONS[range.referenceId], `${metric} referenceId`).toBeTruthy();
expect(range.unit, `${metric} unit`).toBeTruthy();
expect(range.guidanceCaveat, `${metric} guidanceCaveat`).toBeTruthy();
for (const band of range.bands) {
expect(CITATIONS[band.citation], `${metric} band ${band.label}`).toBeTruthy();
// A band must pin at least one bound.
expect(band.low != null || band.high != null, `${metric} ${band.label}`).toBe(true);
}
for (const conflict of range.conflicts ?? []) {
expect(CITATIONS[conflict.citation], `${metric} conflict`).toBeTruthy();
expect(conflict.note).toBeTruthy();
}
}
});
it("keeps ESH 2023 as the BP headline anchor and surfaces US/EU context", () => {
const bp = REFERENCE_RANGES.BLOOD_PRESSURE;
expect(bp.referenceId).toBe("ESH_2023_HYPERTENSION");
expect(referenceLabel("BLOOD_PRESSURE")).toContain("ESH 2023");
// ACC/AHA + ESC 2024 ride only as context, never the headline.
const conflictCitations = (bp.conflicts ?? []).map((c) => c.citation);
expect(conflictCitations).toContain("ACC_AHA_2017_BP");
expect(conflictCitations).toContain("ESC_2024_BP");
});
it("classifies values into the four-state contract", () => {
// Within the normal band.
expect(classifyReference("RESTING_HEART_RATE", 70)).toBe("within");
expect(classifyReference("BLOOD_GLUCOSE", 90)).toBe("within");
// Adjacent watch band.
expect(classifyReference("BLOOD_GLUCOSE", 110)).toBe("slightly-outside");
// Attention band beyond the watch tier.
expect(classifyReference("BLOOD_GLUCOSE", 150)).toBe("outside");
// SpO2 below the healthy floor is attention.
expect(classifyReference("OXYGEN_SATURATION", 88)).toBe("outside");
expect(classifyReference("OXYGEN_SATURATION", 98)).toBe("within");
});
it("returns insufficient for absent values and band-less metrics", () => {
expect(classifyReference("RESTING_HEART_RATE", null)).toBe("insufficient");
expect(classifyReference("RESTING_HEART_RATE", Number.NaN)).toBe("insufficient");
// HRV carries no fixed population band — baseline-only.
expect(REFERENCE_RANGES.HEART_RATE_VARIABILITY.bands).toHaveLength(0);
expect(classifyReference("HEART_RATE_VARIABILITY", 42)).toBe("insufficient");
});
it("steps backbone uses the canonical 8,000 green floor", () => {
const steps = REFERENCE_RANGES.STEPS;
expect(steps.bands[0].low).toBe(8000);
expect(steps.referenceId).toBe("STEPS_SAINT_MAURICE_2020");
});
it("guards + resolvers narrow correctly", () => {
expect(isReferenceMetric("BLOOD_PRESSURE")).toBe(true);
expect(isReferenceMetric("NOT_A_METRIC")).toBe(false);
expect(getReferenceRange("NOT_A_METRIC")).toBeNull();
expect(getReferenceRange("HBA1C")).toBe(REFERENCE_RANGES.HBA1C);
expect(referenceCitation("BLOOD_PRESSURE")).toBe(
CITATIONS.ESH_2023_HYPERTENSION,
);
});
it("no commercial brand appears in any band label or caveat", () => {
const banned = /apple|withings|oura|whoop|garmin|fitbit|polar|samsung/i;
for (const metric of REFERENCE_METRICS) {
const range = REFERENCE_RANGES[metric];
expect(range.guidanceCaveat).not.toMatch(banned);
for (const band of range.bands) expect(band.label).not.toMatch(banned);
for (const conflict of range.conflicts ?? []) {
expect(conflict.note).not.toMatch(banned);
}
}
});
it("metric ids stay a closed, non-empty set", () => {
expect(REFERENCE_METRICS.length).toBeGreaterThan(10);
const ids = new Set<ReferenceMetric>(REFERENCE_METRICS);
expect(ids.size).toBe(REFERENCE_METRICS.length);
});
});
+29 -2
View File
@@ -155,6 +155,11 @@ const REGISTRY: Record<MetricStatusMetricId, MetricStatusMeta> = {
normalRange: { low: 50, high: 100 },
archetype: "physiological-vital",
},
// HRV — no universal "normal" band (ESC/NASPE 1996): only the personal
// trend is meaningful and overnight wearable RMSSD is not interchangeable
// with the clinical 24-h SDNN norms. No `normalRange` by design; the user's
// own baseline leads and single-night swings of ~±10% are routine noise
// (see `reference-ranges.ts HEART_RATE_VARIABILITY.guidanceCaveat`).
HEART_RATE_VARIABILITY: {
id: "HEART_RATE_VARIABILITY",
measurementType: "HEART_RATE_VARIABILITY",
@@ -163,6 +168,13 @@ const REGISTRY: Record<MetricStatusMetricId, MetricStatusMeta> = {
direction: "higher-better",
archetype: "physiological-vital",
},
// SpO₂ — healthy room-air 95–100% (StatPearls/NIH 2023). C4 equity note:
// optical pulse oximeters can OVER-READ true saturation in people with
// darker skin pigmentation (~3× more often than in lighter skin per the
// US FDA 2024 review), so a single near-normal reading is not a clearance.
// The band is unchanged; the caveat lives in the explainer copy +
// `reference-ranges.ts OXYGEN_SATURATION.guidanceCaveat`. Do NOT set a
// "higher is always better" goal — above ~98% adds no benefit.
OXYGEN_SATURATION: {
id: "OXYGEN_SATURATION",
measurementType: "OXYGEN_SATURATION",
@@ -181,13 +193,18 @@ const REGISTRY: Record<MetricStatusMetricId, MetricStatusMeta> = {
normalRange: { low: 12, high: 20 },
archetype: "physiological-vital",
},
// Body temperature — the population oral-equivalent mean is ~36.6 °C, not
// 37.0 °C, with a normal band ~35.7–37.4 °C and fever ≥38.0 °C (J Gen
// Intern Med systematic review, 2019). C6: the high anchor tightens
// 37.5 → 37.2 so the band sits below the fever line; sites differ by up
// to ~1 °C, so the read is a coarse placement only.
BODY_TEMPERATURE: {
id: "BODY_TEMPERATURE",
measurementType: "BODY_TEMPERATURE",
displayName: "Body temperature",
unit: "°C",
direction: "target-band",
normalRange: { low: 36.1, high: 37.5 },
normalRange: { low: 36.1, high: 37.2 },
archetype: "physiological-vital",
},
SKIN_TEMPERATURE: {
@@ -215,12 +232,17 @@ const REGISTRY: Record<MetricStatusMetricId, MetricStatusMeta> = {
direction: "lower-better",
archetype: "physiological-vital",
},
// Pulse-wave velocity — the cf-PWV >10 m/s organ-damage marker is the
// European clinical reference (ESC/ESH 2018). A consumer estimate is a
// PROXY, not interchangeable with clinical cf-PWV tonometry; the band is
// a coarse "below 10 is the reference side" placement only.
PULSE_WAVE_VELOCITY: {
id: "PULSE_WAVE_VELOCITY",
measurementType: "PULSE_WAVE_VELOCITY",
displayName: "Pulse-wave velocity",
unit: "m/s",
direction: "lower-better",
normalRange: { low: 0, high: 10 },
archetype: "physiological-vital",
},
VASCULAR_AGE: {
@@ -232,13 +254,18 @@ const REGISTRY: Record<MetricStatusMetricId, MetricStatusMeta> = {
archetype: "physiological-vital",
},
// ── activity-fitness ──
// Steps — C2/D4 reconcile: the canonical "green" floor is 8,000/day
// (Saint-Maurice JAMA 2020: mortality risk falls steeply toward ~8k with
// continued benefit through ~12k), aligning this display band with the
// resolver + classifier so the cross-surface number stops contradicting.
// The "10,000" target is a marketing slogan, not research.
STEPS: {
id: "STEPS",
measurementType: "ACTIVITY_STEPS",
displayName: "Steps",
unit: "steps/day",
direction: "higher-better",
normalRange: { low: 7000, high: 10000 },
normalRange: { low: 8000, high: 15000 },
archetype: "activity-fitness",
},
ACTIVE_ENERGY: {
+287
View File
@@ -189,6 +189,293 @@ export const CITATIONS = {
caveat:
"Reference values for an idealised adult; individual variation broad.",
},
// ── v1.18.6 — reference-range backbone sources ──
/**
* AHA — All About Heart Rate (resting + target heart rates), 2024.
* Normal adult resting 60–100 bpm; endurance athletes benignly 40–60.
* Consumer-facing orientation; symptomatic concern is the trigger for
* care, not the number alone.
*/
AHA_2024_RHR: {
id: "aha-2024-rhr",
name: "AHA 2024 Heart Rate",
year: 2024,
url: "https://www.heart.org/en/health-topics/high-blood-pressure/the-facts-about-high-blood-pressure/all-about-heart-rate-pulse",
caveat:
"General adult band; a trained heart can sit 40–60, and a new low with dizziness/fainting is the concern, not the number alone.",
},
/**
* StatPearls/NIH — Pulse Oximetry (2023). Healthy adult room-air SpO₂
* 95–100% (bulk 96–99%); mild hypoxaemia 91–94%; clinically significant
* <90%. Consumer wrist/finger optical readers are wellness-grade
* (±2–4%) and a single low reading is often artefact.
*/
STATPEARLS_PULSE_OX: {
id: "statpearls-pulse-ox",
name: "StatPearls Pulse Oximetry",
year: 2023,
url: "https://www.ncbi.nlm.nih.gov/books/NBK470348/",
caveat:
"Healthy room-air band; consumer optical SpO₂ is ±2–4% and a single low reading is often artefact — sustained runs are the signal.",
},
/**
* 2017 ACC/AHA Guideline for the Prevention, Detection, Evaluation and
* Management of High Blood Pressure in Adults. Lowers the US disease
* line: Normal <120/<80; Elevated 120–129/<80; Stage 1 130–139 or
* 80–89; Stage 2 ≥140 or ≥90. Surfaced only as the stricter COMPARISON
* anchor next to the shipped ESH default — never the labelled band.
*/
ACC_AHA_2017_BP: {
id: "acc-aha-2017-bp",
name: "ACC/AHA 2017",
year: 2017,
url: "https://www.ahajournals.org/doi/10.1161/HYP.0000000000000065",
caveat:
"US guideline; labels 130–139/80–89 as Stage 1 — a stricter line than the ESH default, shown for placement only.",
},
/**
* 2024 ESC Guidelines for the management of elevated blood pressure and
* hypertension. Adds an "Elevated" diagnostic band (120–139/70–89) and a
* treated SBP target of 120–129. Disease line stays ≥140/90 office.
* Displayed as context alongside the ESH 2023 default.
*/
ESC_2024_BP: {
id: "esc-2024-bp",
name: "ESC 2024",
year: 2024,
url: "https://academic.oup.com/eurheartj/article/45/38/3912/7741010",
caveat:
"European; introduces an 'Elevated' 120–139/70–89 band and a treated 120–129 target — context, not the shipped classification.",
},
/**
* StatPearls/NCBI — Physiology, Pulse Pressure (NBK482408). PP = SBP −
* DBP; ~40 mmHg typical at rest, wide ≳60 mmHg marks large-artery
* stiffening, narrow ≲25 mmHg can accompany low stroke volume. A
* "discuss with a clinician" signal, not a diagnosis.
*/
STATPEARLS_PULSE_PRESSURE: {
id: "statpearls-pulse-pressure",
name: "StatPearls Pulse Pressure",
year: 2023,
url: "https://www.ncbi.nlm.nih.gov/books/NBK482408/",
caveat:
"Educational physiology reference; PP is a discussion prompt, never a standalone diagnosis.",
},
/**
* StatPearls/NCBI — Physiology, Mean Arterial Pressure (NBK538226).
* MAP ≈ (SBP + 2·DBP)/3; resting band ~70–100 mmHg, ~60 mmHg the
* conventional organ-perfusion floor — an acute-care concept, NOT a
* consumer self-alarm threshold.
*/
STATPEARLS_MAP: {
id: "statpearls-map",
name: "StatPearls Mean Arterial Pressure",
year: 2023,
url: "https://www.ncbi.nlm.nih.gov/books/NBK538226/",
caveat:
"The ~60 mmHg floor is an acute-care perfusion concept, not a consumer alarm threshold.",
},
/**
* 2018 ESC/ESH Guidelines for the management of arterial hypertension.
* Source of the carotid-femoral PWV >10 m/s organ-damage cut-off and
* the BP grade bands still widely cited. The PWV number is European;
* AHA/ACC do not enshrine an equivalent consumer PWV threshold.
*/
ESC_ESH_2018_PWV: {
id: "esc-esh-2018-pwv",
name: "ESC/ESH 2018",
year: 2018,
url: "https://academic.oup.com/eurheartj/article/39/33/3021/5079119",
caveat:
"cf-PWV >10 m/s is the European clinical reference; consumer PWV / vascular-age estimates are proxies, not clinical cf-PWV.",
},
/**
* US FDA — pulse-oximeter accuracy and limitations (Feb 2024 advisory
* committee review + safety communication). Oximeters can overestimate
* true saturation in people with darker skin, roughly 3× more often
* than in lighter-skinned patients (occult hypoxaemia).
*/
FDA_2024_PULSE_OX: {
id: "fda-2024-pulse-ox",
name: "FDA 2024 Pulse Oximeter Review",
year: 2024,
url: "https://www.fda.gov/medical-devices/safety-communications/pulse-oximeter-accuracy-and-limitations-fda-safety-communication",
caveat:
"Documents over-reading in darker skin; a single near-normal reading is not a clearance — confirm when concerned.",
},
/**
* "Normal Body Temperature: A Systematic Review" — J Gen Intern Med,
* 2019. Population oral-equivalent mean ~36.6 °C (normal ~35.7–37.4 °C),
* superseding the conventional 37.0 °C set point. Fever ≥38.0 °C oral.
*/
JGIM_2019_TEMPERATURE: {
id: "jgim-2019-temperature",
name: "J Gen Intern Med 2019",
year: 2019,
url: "https://link.springer.com/article/10.1007/s11606-019-05148-7",
caveat:
"Oral-equivalent mean; sites differ by up to ~1 °C — tag the measurement site before comparing.",
},
/**
* Royal College of Physicians — National Early Warning Score 2 (NEWS2),
* 2017. Respiratory-rate scoring: highest-risk ≤8 and ≥25/min. A
* hospital deterioration tool, not a consumer alarm.
*/
RCP_2017_NEWS2: {
id: "rcp-2017-news2",
name: "RCP NEWS2 2017",
year: 2017,
url: "https://www.rcp.ac.uk/improving-care/resources/national-early-warning-score-news-2/",
caveat:
"Hospital deterioration score; the ≥25/min cutoff is clinical context, not a self-monitoring alarm.",
},
/**
* American Lung Association — resting respiratory rate reference
* (12–20 breaths/min for healthy adults). Consumer-facing vital-sign
* orientation.
*/
ALA_RESPIRATORY_RATE: {
id: "ala-respiratory-rate",
name: "American Lung Association",
year: 2024,
url: "https://www.lung.org/lung-health-diseases/how-lungs-work/breathing-rate",
caveat:
"General adult resting band; rates run higher in children and shift with activity, fever and emotion.",
},
/**
* Peer-reviewed visceral-adipose-tissue imaging literature (2014–2025):
* CT/MRI/DXA VAT area >~100 cm² increased-risk, >~160 cm² high-risk.
* Ethnicity-dependent; the consumer scale "rating" is NOT a cm² value.
*/
VAT_IMAGING_THRESHOLD: {
id: "vat-imaging-threshold",
name: "VAT imaging literature",
year: 2025,
url: "https://pubmed.ncbi.nlm.nih.gov/24008002/",
caveat:
"Imaging cm² thresholds; the consumer scale 'visceral fat rating' is a separate unitless scale, not interchangeable.",
},
/**
* Wilcox AJ, Dunson D, Baird DD. "The timing of the 'fertile window' in
* the menstrual cycle." BMJ, 2000. The 6-day fertile window ends on
* ovulation day; ovulation timing is highly variable even in regular
* cyclers.
*/
WILCOX_2000_FERTILE_WINDOW: {
id: "wilcox-2000-fertile-window",
name: "Wilcox BMJ 2000",
year: 2000,
url: "https://www.bmj.com/content/321/7271/1259",
caveat:
"Fertile-window timing is highly variable; calendar estimates approximate and are not a contraceptive method.",
},
/**
* ACOG Committee Opinion 651 (2015) — "Menstruation in Girls and
* Adolescents: Using the Menstrual Cycle as a Vital Sign." Adult cycle
* 24–38 days, bleeding ~5 days (up to ~7).
*
* NEEDS-VERIFY ACOG — the cycle-length / bleeding-duration figures cited
* in the `insights.subPage.explainer.cycleEducationBody` copy, and any
* "when to see a clinician" thresholds (cycle <21 / >35, amenorrhea ≥3
* months, heavy bleeding >7 days), must be confirmed against the live
* ACOG FAQ before shipping that copy to users (automated fetch was
* rate-limited during research).
*/
ACOG_CO651_2015: {
id: "acog-co651-2015",
name: "ACOG CO 651 2015",
year: 2015,
url: "https://www.acog.org/clinical/clinical-guidance/committee-opinion/articles/2015/12/menstruation-in-girls-and-adolescents-using-the-menstrual-cycle-as-a-vital-sign",
caveat:
"Cycle as a vital sign; exact clinician-consultation thresholds should be confirmed against the live ACOG FAQ.",
},
/**
* Phillips AJK, et al. "Irregular sleep/wake patterns are associated
* with poorer academic performance and delayed circadian and sleep/wake
* timing." Sci Rep, 2017 — origin of the Sleep Regularity Index (SRI).
* UK Biobank (Windred 2023/24) tied higher regularity to lower mortality.
*/
PHILLIPS_2017_SRI: {
id: "phillips-2017-sri",
name: "Phillips Sci Rep 2017",
year: 2017,
url: "https://www.nature.com/articles/s41598-017-03171-4",
caveat:
"Sleep-regularity construct; no universal good/bad threshold — present as a personal trend.",
},
/**
* US CDC — About Sleep / tips for better sleep (2024). Evidence-based
* sleep-hygiene behaviours (consistent schedule, morning light, dim
* evenings, caffeine timing, cool dark room).
*/
CDC_2024_SLEEP: {
id: "cdc-2024-sleep",
name: "CDC 2024 Sleep",
year: 2024,
url: "https://www.cdc.gov/sleep/about/index.html",
caveat: "General sleep-hygiene guidance; individual needs vary.",
},
/**
* WHO — Obesity: preventing and managing the global epidemic (TRS 894),
* 2000. The BMI classification still in use: underweight <18.5, normal
* 18.5–24.9, overweight 25.0–29.9, obesity ≥30. Asian-population risk
* rises at lower cut-offs (WHO 2004).
*/
WHO_2000_BMI: {
id: "who-2000-bmi",
name: "WHO 2000 BMI",
year: 2000,
url: "https://www.who.int/publications/i/item/WHO_TRS_894",
caveat:
"Population screen, not a diagnosis; Asian-population risk rises at lower cut-offs and BMI does not distinguish muscle from fat.",
},
/**
* WHO/IDF — Definition and Diagnosis of Diabetes Mellitus and
* Intermediate Hyperglycaemia, 2006. Impaired fasting glucose floor of
* 110 mg/dL (vs ADA's 100); the diabetes line (≥126) is shared.
*/
WHO_IDF_2006_GLUCOSE: {
id: "who-idf-2006-glucose",
name: "WHO/IDF 2006",
year: 2006,
url: "https://www.who.int/publications/i/item/definition-and-diagnosis-of-diabetes-mellitus-and-intermediate-hyperglycaemia",
caveat:
"Sets the impaired-fasting-glucose floor at 110 mg/dL; the diabetes threshold (≥126) is shared with ADA.",
},
/**
* ESC/NASPE Task Force — Heart Rate Variability: Standards of
* Measurement, Physiological Interpretation and Clinical Use.
* Circulation / Eur Heart J, 1996. Defines SDNN/RMSSD norms and the
* prognostic 24-h SDNN bands; day-to-day swings of ~±10% are routine.
*/
ESC_NASPE_1996_HRV: {
id: "esc-naspe-1996-hrv",
name: "ESC/NASPE HRV Standards 1996",
year: 1996,
url: "https://www.ahajournals.org/doi/10.1161/01.CIR.93.5.1043",
caveat:
"Clinical 24-h SDNN norms are NOT interchangeable with an overnight wearable RMSSD; only the personal trend is meaningful.",
},
} as const satisfies Record<string, Citation>;
export type CitationId = keyof typeof CITATIONS;
+377
View File
@@ -0,0 +1,377 @@
/**
* v1.18.6 — citation-backed reference-range backbone.
*
* A single structured source of truth for the population reference bands
* the UI surfaces (explainer captions, metric tiles, the glossary, the
* "where your value sits" tooltip). Each metric carries:
* - the canonical display unit,
* - one or more named `bands` (low/high half-open, with the body + year
* that publishes them via a `CitationId`),
* - the primary `referenceId` (the body the headline band is sourced to),
* - an optional `conflicts[]` of alternative-guideline context (e.g. the
* BP US/EU disagreement — shown as placement context, never the
* shipped classification),
* - a `guidanceCaveat` that frames the band as general guidance.
*
* This module is intentionally read-only data + pure helpers — no DB, no
* LLM, no per-user state. The metric-status registry fills its coarse
* `normalRange` from these bands; the BP detail surface reads the PP / MAP
* anchors; the explainers read `referenceLabel()` for the cited sentence.
*
* Every band traces to `src/lib/medical-citations.ts`. No commercial brand
* is named anywhere. Ranges are population anchors that shift with age,
* sex and condition — the user's own baseline always leads the read; these
* are a placement aid, not a diagnosis. Where authoritative bodies
* disagree the disagreement is surfaced (`conflicts`), never silently
* resolved.
*/
import { CITATIONS, type CitationId, citationLabel } from "./medical-citations";
/**
* A single named reference band. `low`/`high` are inclusive display
* anchors in the metric's unit; either bound may be omitted for an
* open-ended band (e.g. "≥7 h", "<100 mg/dL").
*/
export interface ReferenceBand {
/** Stable, human-readable band label ("Normal", "Elevated", "Fever"). */
readonly label: string;
readonly low?: number;
readonly high?: number;
/** The body + year that publishes this band. */
readonly citation: CitationId;
}
/**
* An alternative-guideline framing shown as CONTEXT next to the default
* band — never the shipped classification. The BP US/EU split is the
* canonical use.
*/
export interface ReferenceConflict {
/** Short note describing the alternative framing (general education). */
readonly note: string;
readonly citation: CitationId;
}
export interface ReferenceRange {
/** Canonical display unit (matches the metric tile + registry). */
readonly unit: string;
/** Ordered population bands; the first is the "good"/normal placement. */
readonly bands: readonly ReferenceBand[];
/** The body the headline band is sourced to (drives the cited sentence). */
readonly referenceId: CitationId;
/** Alternative-guideline context, shown for placement only. */
readonly conflicts?: readonly ReferenceConflict[];
/** One-sentence general-guidance frame surfaced under the band. */
readonly guidanceCaveat: string;
}
/**
* The reference-metric slugs the backbone covers. Stable string keys (not
* the DB enum) so the UI, the registry and the explainer can all read one
* vocabulary. A superset of the universal metrics + the cardiovascular
* derivations the explainers cite.
*/
export type ReferenceMetric =
| "BLOOD_PRESSURE"
| "PULSE_PRESSURE"
| "MEAN_ARTERIAL_PRESSURE"
| "PULSE_WAVE_VELOCITY"
| "RESTING_HEART_RATE"
| "HEART_RATE_VARIABILITY"
| "OXYGEN_SATURATION"
| "RESPIRATORY_RATE"
| "BODY_TEMPERATURE"
| "BLOOD_GLUCOSE"
| "HBA1C"
| "BMI"
| "STEPS"
| "VISCERAL_FAT"
| "SLEEP_DURATION";
export const REFERENCE_RANGES: Record<ReferenceMetric, ReferenceRange> = {
// ── Blood pressure — ESH 2023 is the SHIPPED default (do not flip). ──
// ACC/AHA 2017 + ESC 2024 are surfaced only as placement CONTEXT.
BLOOD_PRESSURE: {
unit: "mmHg",
bands: [
// ESH grade bands (office, mmHg, systolic anchors shown).
{ label: "Optimal", high: 120, citation: "ESH_2023_HYPERTENSION" },
{ label: "Normal", low: 120, high: 129, citation: "ESH_2023_HYPERTENSION" },
{ label: "High-normal", low: 130, high: 139, citation: "ESH_2023_HYPERTENSION" },
{ label: "Hypertension", low: 140, citation: "ESH_2023_HYPERTENSION" },
],
referenceId: "ESH_2023_HYPERTENSION",
conflicts: [
{
note: "ACC/AHA (US) labels 130–139/80–89 as Stage 1 hypertension — a stricter line than the European disease threshold of 140/90.",
citation: "ACC_AHA_2017_BP",
},
{
note: "ESC 2024 adds an 'Elevated' band at 120–139/70–89 and a treated target of 120–129.",
citation: "ESC_2024_BP",
},
],
guidanceCaveat:
"Categorised from a standardised seated reading or a validated home average — never a single reading. Targets individualise by age and condition.",
},
// Pulse pressure = SBP − DBP. Pure derivation of stored BP.
PULSE_PRESSURE: {
unit: "mmHg",
bands: [
{ label: "Narrow", high: 25, citation: "STATPEARLS_PULSE_PRESSURE" },
{ label: "Typical at rest", low: 25, high: 60, citation: "STATPEARLS_PULSE_PRESSURE" },
{ label: "Wide", low: 60, citation: "STATPEARLS_PULSE_PRESSURE" },
],
referenceId: "STATPEARLS_PULSE_PRESSURE",
guidanceCaveat:
"A wide gap (≳60) in middle age can reflect arterial stiffening; a narrow gap (≲25) can accompany low stroke volume. A discussion prompt, not a diagnosis.",
},
// Mean arterial pressure ≈ (SBP + 2·DBP)/3.
MEAN_ARTERIAL_PRESSURE: {
unit: "mmHg",
bands: [
{ label: "Resting band", low: 70, high: 100, citation: "STATPEARLS_MAP" },
],
referenceId: "STATPEARLS_MAP",
guidanceCaveat:
"The average pressure your organs see across a heartbeat. The ~60 mmHg floor is an acute-care concept, not a consumer self-alarm.",
},
PULSE_WAVE_VELOCITY: {
unit: "m/s",
bands: [
{ label: "Reference", high: 10, citation: "ESC_ESH_2018_PWV" },
{ label: "Elevated stiffness", low: 10, citation: "ESC_ESH_2018_PWV" },
],
referenceId: "ESC_ESH_2018_PWV",
guidanceCaveat:
"The cf-PWV >10 m/s organ-damage marker is a European clinical reference measured by tonometry; a consumer estimate is a proxy, not interchangeable.",
},
// ── Resting heart rate — uses RESTING_HEART_RATE, never workout pulse. ──
// Bands are authored normal-first so `classifyReference` treats the
// population-normal band as the "within" placement. The athletic band
// (40–60) is benign for a trained heart but sits below the standard
// normal range — the caveat copy carries that nuance.
RESTING_HEART_RATE: {
unit: "bpm",
bands: [
{ label: "Normal adult resting", low: 60, high: 100, citation: "AHA_2024_RHR" },
{ label: "Athletic (benign)", low: 40, high: 60, citation: "AHA_2024_RHR" },
],
referenceId: "AHA_2024_RHR",
guidanceCaveat:
"Measured at rest, ideally on waking. A trained heart can sit 40–60 bpm; the trend over weeks beats any single reading. Caffeine, stress, fever and medication all shift it.",
},
HEART_RATE_VARIABILITY: {
unit: "ms",
bands: [],
referenceId: "ESC_NASPE_1996_HRV",
guidanceCaveat:
"There is no universal 'normal' — only your own trend is meaningful. Single-night swings of about ±10% are routine; track a 7-day rolling average and read changes smaller than that as noise.",
},
// ── SpO2 — healthy room-air band + darker-skin over-read caveat (C4). ──
OXYGEN_SATURATION: {
unit: "%",
bands: [
{ label: "Healthy room air", low: 95, high: 100, citation: "STATPEARLS_PULSE_OX" },
{ label: "Mild hypoxaemia", low: 91, high: 94, citation: "STATPEARLS_PULSE_OX" },
],
referenceId: "STATPEARLS_PULSE_OX",
guidanceCaveat:
"95–100% is normal at rest; above ~98% adds no benefit. Optical readers can over-read true saturation in people with darker skin, so a single near-normal reading is not a clearance — confirm when warm, still and concerned.",
},
RESPIRATORY_RATE: {
unit: "breaths/min",
bands: [
{ label: "Normal resting adult", low: 12, high: 20, citation: "ALA_RESPIRATORY_RATE" },
{ label: "Worth attention", low: 22, citation: "RCP_2017_NEWS2" },
],
referenceId: "ALA_RESPIRATORY_RATE",
guidanceCaveat:
"Meaningful only truly at rest or asleep. A sustained rise of about +3 above your own nightly baseline can be an early illness signal; clinical escalation cutoffs are hospital tools, not self-alarms.",
},
// ── Body temperature — evidence-based 36.6 °C mean, fever ≥38 (C6). ──
BODY_TEMPERATURE: {
unit: "°C",
bands: [
{ label: "Normal oral range", low: 35.7, high: 37.4, citation: "JGIM_2019_TEMPERATURE" },
{ label: "Fever", low: 38.0, citation: "JGIM_2019_TEMPERATURE" },
],
referenceId: "JGIM_2019_TEMPERATURE",
guidanceCaveat:
"The population mean is ~36.6 °C, not 37.0 °C, and swings ~0.5 °C across the day. Sites differ by up to ~1 °C, so tag the measurement site. Wearable wrist readings track relative change, not a fever value.",
},
// ── Glucose — GENERAL non-diabetic bands only (diabetes opt-in = W6). ──
BLOOD_GLUCOSE: {
unit: "mg/dL",
bands: [
{ label: "Normal fasting", high: 100, citation: "ADA_2024_GLYCEMIC" },
{ label: "Prediabetes (fasting)", low: 100, high: 125, citation: "ADA_2024_GLYCEMIC" },
{ label: "Diabetes range (fasting)", low: 126, citation: "ADA_2024_GLYCEMIC" },
],
referenceId: "ADA_2024_GLYCEMIC",
conflicts: [
{
note: "WHO/IDF set the prediabetes fasting floor at 110 mg/dL rather than 100; the diabetes line (≥126) is shared.",
citation: "WHO_IDF_2006_GLUCOSE",
},
],
guidanceCaveat:
"Fasting bands for adults without diabetes. A single high reading does not diagnose — a clinician confirms with a repeat test. People managing diabetes use different, clinician-set targets.",
},
HBA1C: {
unit: "%",
bands: [
{ label: "Normal", high: 5.7, citation: "ADA_2024_GLYCEMIC" },
{ label: "Prediabetes", low: 5.7, high: 6.4, citation: "ADA_2024_GLYCEMIC" },
{ label: "Diabetes", low: 6.5, citation: "ADA_2024_GLYCEMIC" },
],
referenceId: "ADA_2024_GLYCEMIC",
guidanceCaveat:
"Reflects the average glucose over ~2–3 months. Unreliable with altered red-cell turnover; when discordant with glucose, glucose takes precedence.",
},
BMI: {
unit: "kg/m²",
bands: [
{ label: "Underweight", high: 18.5, citation: "WHO_2020_PA" },
{ label: "Normal", low: 18.5, high: 24.9, citation: "WHO_2020_PA" },
{ label: "Overweight", low: 25.0, high: 29.9, citation: "WHO_2020_PA" },
{ label: "Obesity", low: 30.0, citation: "WHO_2020_PA" },
],
referenceId: "WHO_2000_BMI",
guidanceCaveat:
"A population-level screen, not a diagnosis — it does not distinguish muscle from fat or where fat sits. Asian-population risk rises at lower cut-offs.",
},
// ── Steps — canonical green floor = 8,000 (C2/D4 reconcile). ──
STEPS: {
unit: "steps/day",
bands: [
{ label: "Higher mortality benefit", low: 8000, citation: "STEPS_SAINT_MAURICE_2020" },
],
referenceId: "STEPS_SAINT_MAURICE_2020",
guidanceCaveat:
"Mortality risk falls steeply toward ~8,000 steps/day with continued benefit through ~12,000; the optimal count is lower for older adults. The '10,000' figure is a marketing slogan, not research.",
},
VISCERAL_FAT: {
unit: "rating",
bands: [
{ label: "Healthy rating", low: 1, high: 12, citation: "VAT_IMAGING_THRESHOLD" },
{ label: "Elevated rating", low: 13, citation: "VAT_IMAGING_THRESHOLD" },
],
referenceId: "VAT_IMAGING_THRESHOLD",
guidanceCaveat:
"The consumer scale 'rating' (≈1–12 healthy) is a unitless scale, not the imaging cm² value (>100 cm² increased risk). High visceral fat is closely tied to heart disease and type 2 diabetes.",
},
// ── Sleep — adult 7–9 h; total sleep time beats the stage split. ──
SLEEP_DURATION: {
unit: "h",
bands: [
{ label: "Adult 18–64", low: 7, high: 9, citation: "AASM_2015_SLEEP" },
{ label: "Older adult 65+", low: 7, high: 8, citation: "AASM_2015_SLEEP" },
],
referenceId: "AASM_2015_SLEEP",
guidanceCaveat:
"Both short and long habitual sleep carry risk. Regularity predicts health at least as strongly as duration, and a wearable's total sleep time is far more trustworthy than its deep/REM split.",
},
};
/** All reference-metric slugs the backbone covers. */
export const REFERENCE_METRICS = Object.keys(
REFERENCE_RANGES,
) as ReferenceMetric[];
/** Type guard narrowing an arbitrary string to a covered reference metric. */
export function isReferenceMetric(value: string): value is ReferenceMetric {
return Object.prototype.hasOwnProperty.call(REFERENCE_RANGES, value);
}
/** Resolve the reference range for a metric, or null when uncovered. */
export function getReferenceRange(metric: string): ReferenceRange | null {
return isReferenceMetric(metric) ? REFERENCE_RANGES[metric] : null;
}
/**
* Localised "ESH 2023 (2023)"-style label for the headline band's source.
* The citation name is canonical and not translated (locale-agnostic).
*/
export function referenceLabel(metric: ReferenceMetric): string {
return citationLabel(REFERENCE_RANGES[metric].referenceId);
}
/**
* The four-state "where your value sits" phrasing contract (UI-1). A
* deterministic placement of a value against the metric's population
* bands, ALWAYS framed as general guidance. Consumers map each state to a
* cited i18n string; the state itself never asserts a diagnosis.
*
* - `within` — inside the normal/good band.
* - `slightly-outside`— in an adjacent watch band (e.g. high-normal,
* prediabetes, mild hypoxaemia).
* - `outside` — in a band the guideline flags for attention.
* - `insufficient` — no value, or the metric carries no population band
* (device-derived / baseline-only signals).
*/
export type ReferencePlacement =
| "within"
| "slightly-outside"
| "outside"
| "insufficient";
/**
* Place a value against a metric's population bands. Returns
* `insufficient` when the value is absent or the metric has no fixed band
* (HRV and other device-derived signals defer wholly to the personal
* baseline). Pure + side-effect free; the user's own baseline still leads
* the read — this is only a coarse placement aid.
*/
export function classifyReference(
metric: ReferenceMetric,
value: number | null | undefined,
): ReferencePlacement {
const range = REFERENCE_RANGES[metric];
if (value == null || !Number.isFinite(value) || range.bands.length === 0) {
return "insufficient";
}
// Find the first band whose half-open interval contains the value.
// Bands are authored in ascending order; the FIRST band is the
// normal/good placement, so its membership maps to `within`.
for (let i = 0; i < range.bands.length; i++) {
const band = range.bands[i];
const aboveLow = band.low == null || value >= band.low;
const belowHigh = band.high == null || value <= band.high;
if (aboveLow && belowHigh) {
if (i === 0) return "within";
// The band immediately adjacent to normal is the watch tier; any
// band beyond it is the attention tier.
return i === 1 ? "slightly-outside" : "outside";
}
}
// Value sits beyond every authored band (e.g. below an open-low normal,
// or above the top band's open-high). Treat an under-shoot of the first
// band's low as within when the first band is open-low; otherwise the
// value is outside the covered range → attention.
const first = range.bands[0];
if (first.low != null && value < first.low) return "outside";
return "outside";
}
/**
* Convenience: the citation entry backing a metric's headline band, for
* surfaces that want the url + caveat rather than the formatted label.
*/
export function referenceCitation(metric: ReferenceMetric) {
return CITATIONS[REFERENCE_RANGES[metric].referenceId];
}