Files
HealthLog/src/lib/ai/medical-references.ts
T
Marc-André Bombeck fea1d1e0b5 chore: strip planning tags from comments and enable the rule
Output of `node scripts/strip-planning-tags.mjs --write`: 3,157 comment
lines in 1,308 files lose the work-item identifier at their head. Release
versions and issue references stay; nothing outside comment text
changes, which the script checks per file against a comment-free
printout. healthlog/no-planning-tags is enabled at error level with this
commit, so the tree it lands on is clean.
2026-10-20 09:08:40 +02:00

201 lines
7.5 KiB
TypeScript

/**
* v1.4.16 — curated medical-reference bundle.
*
* Every normative claim in an AI-generated recommendation ("target
* < 140/90", "BMI 18.5-24.9", "≥ 7h sleep") MUST cite an entry in
* this list via `recommendation.referenceId`. The schema in
* `src/lib/ai/schema.ts` validates `referenceId` against
* `MEDICAL_REFERENCE_IDS`, so the model cannot fabricate a guideline
* citation pointing at a source that does not exist.
*
* Curation policy:
* - Only major bodies: AHA, ESH, ESC, WHO, DGE (German nutrition
* society), ADA. No advocacy groups, no SEO health sites, no
* research previews.
* - URLs go directly to the guideline page, not to a press release.
* They open in a new tab via `target="_blank" rel="noreferrer"`.
* - publishedYear matches the document version we cite. When a body
* refreshes the guideline, bump the year and update the URL in the
* same commit.
* - Locale: `title` is English-canonical, `titleDe` is the German
* UI label. The body of the guideline itself is whatever language
* the publishing body uses; the URL is the truth.
*
* The shape is intentionally narrower than the full §1.4.16 roadmap's
* "embedded excerpt" plan — that is a v1.4.17+ enhancement and would
* require fair-use-length quotations, locale-routed excerpts, and a
* scrape pipeline. v1.4.16 ships the citation grounding only: the
* model emits an id, the UI renders a labelled link.
*/
export type MedicalReferenceOrg = "AHA" | "ESH" | "ESC" | "WHO" | "DGE" | "ADA";
export type MedicalReferenceMetric =
"bp" | "weight" | "pulse" | "mood" | "medication";
export interface MedicalReference {
/** Stable slug — used as `recommendation.referenceId`. */
id: string;
/** Issuing body. Stable contract value, do not translate. */
org: MedicalReferenceOrg;
/** Title in English (canonical UI text). */
title: string;
/** Title in German for the de-locale UI footnote. */
titleDe: string;
/** Direct link to the guideline page. https only. */
url: string;
/** Year the guideline / position paper was published. */
publishedYear: number;
/**
* Fine-grained scope tags — used by future filtering / display
* logic. Free-form strings; not validated.
*/
scope: string[];
/**
* High-level metric buckets the reference applies to. The prompt
* builder picks references whose buckets overlap the metrics in the
* current snapshot.
*/
metricApplicability: MedicalReferenceMetric[];
}
export const MEDICAL_REFERENCES: MedicalReference[] = [
{
id: "esh-2023-bp-adults",
org: "ESH",
title: "ESH 2023 Guidelines for the Management of Arterial Hypertension",
titleDe: "ESH-Leitlinie 2023 zur Behandlung der arteriellen Hypertonie",
url: "https://www.eshonline.org/guidelines/2023-esh-guidelines/",
publishedYear: 2023,
scope: ["bp_target", "bp_classification", "lifestyle"],
metricApplicability: ["bp", "medication"],
},
{
id: "esc-2024-bp-cv",
org: "ESC",
title:
"ESC 2024 Guidelines for the Management of Elevated Blood Pressure and Hypertension",
titleDe:
"ESC-Leitlinie 2024 zur Behandlung von erhöhtem Blutdruck und Hypertonie",
url: "https://www.escardio.org/Guidelines/Clinical-Practice-Guidelines/2024-Guidelines-on-Elevated-BP-and-Hypertension",
publishedYear: 2024,
scope: ["bp_target", "bp_thresholds", "cv_risk"],
metricApplicability: ["bp", "pulse", "medication"],
},
{
id: "aha-2017-hypertension",
org: "AHA",
title:
"ACC/AHA 2017 Guideline for the Prevention, Detection, Evaluation and Management of High Blood Pressure in Adults",
titleDe:
"ACC/AHA-Leitlinie 2017 zur Prävention, Erkennung, Bewertung und Behandlung von Bluthochdruck bei Erwachsenen",
url: "https://www.ahajournals.org/doi/10.1161/HYP.0000000000000065",
publishedYear: 2017,
scope: ["bp_target", "bp_classification"],
metricApplicability: ["bp"],
},
{
id: "who-2021-hypertension",
org: "WHO",
title:
"WHO Guideline for the Pharmacological Treatment of Hypertension in Adults",
titleDe:
"WHO-Leitlinie zur pharmakologischen Behandlung der Hypertonie bei Erwachsenen",
url: "https://www.who.int/publications/i/item/9789240033986",
publishedYear: 2021,
scope: ["bp_target", "treatment", "lifestyle"],
metricApplicability: ["bp", "medication"],
},
{
id: "who-2024-bmi-classification",
org: "WHO",
title: "WHO BMI classification for adults",
titleDe: "WHO-BMI-Klassifikation für Erwachsene",
url: "https://www.who.int/europe/news-room/fact-sheets/item/a-healthy-lifestyle---who-recommendations",
publishedYear: 2024,
scope: ["bmi_classification", "weight_target"],
metricApplicability: ["weight"],
},
{
id: "dge-2024-healthy-eating",
org: "DGE",
title: "DGE recommendations for a wholesome diet",
titleDe: "DGE-Empfehlungen für eine vollwertige Ernährung",
url: "https://www.dge.de/gesunde-ernaehrung/dge-empfehlungen/",
publishedYear: 2024,
scope: ["nutrition", "weight_target", "lifestyle"],
metricApplicability: ["weight", "mood"],
},
{
id: "aha-2023-resting-heart-rate",
org: "AHA",
title: "AHA — All About Heart Rate (Pulse)",
titleDe: "AHA — Alles über die Herzfrequenz (Puls)",
url: "https://www.heart.org/en/health-topics/high-blood-pressure/the-facts-about-high-blood-pressure/all-about-heart-rate-pulse",
publishedYear: 2023,
scope: ["pulse_target", "resting_heart_rate"],
metricApplicability: ["pulse"],
},
];
/** Stable list of all valid `recommendation.referenceId` values. */
export const MEDICAL_REFERENCE_IDS: readonly string[] = MEDICAL_REFERENCES.map(
(r) => r.id,
);
/** Stable list of all distinct issuing bodies. */
export const MEDICAL_REFERENCE_ORGS: readonly MedicalReferenceOrg[] =
Array.from(new Set(MEDICAL_REFERENCES.map((r) => r.org)));
/**
* Look up a reference by id. Returns `undefined` for unknown ids — the
* UI uses this to silently drop a footnote rather than render a broken
* link if a payload sneaks through with an unknown id (defence in
* depth alongside the schema check).
*/
export function getMedicalReferenceById(
id: string,
): MedicalReference | undefined {
return MEDICAL_REFERENCES.find((r) => r.id === id);
}
/**
* Map the set of feature/snapshot sections present for a generation onto
* the reference metric buckets. Lets the insight consumers pick the
* applicable curated SOURCES without re-encoding the bucket names.
*
* `present` carries one boolean per high-level section the snapshot
* actually contains; absent / false sections drop their bucket so a
* weight-only generation never lists ESH BP guidance.
*/
export function metricsFromPresentSections(present: {
bloodPressure?: boolean;
weight?: boolean;
pulse?: boolean;
mood?: boolean;
medication?: boolean;
}): MedicalReferenceMetric[] {
const out: MedicalReferenceMetric[] = [];
if (present.bloodPressure) out.push("bp");
if (present.weight) out.push("weight");
if (present.pulse) out.push("pulse");
if (present.mood) out.push("mood");
if (present.medication) out.push("medication");
return out;
}
/**
* Pick the references whose `metricApplicability` overlaps any of the
* supplied metric buckets. Order preserved from the canonical bundle
* for stable prompt rendering.
*/
export function selectReferencesForMetrics(
metrics: readonly MedicalReferenceMetric[],
): MedicalReference[] {
if (metrics.length === 0) return [];
const wanted = new Set(metrics);
return MEDICAL_REFERENCES.filter((r) =>
r.metricApplicability.some((m) => wanted.has(m)),
);
}