feat(insights): inject guideline band placement into generic metric assessments

Thread the interpretation block into the generic per-metric assessment
path (the ~40 HealthKit metric cards). For a metric the knowledge base
covers, the prompt now carries the band table plus the server-computed
position of the current value, so the assessment names where the value
sits on the guideline scale and judges the trend by that position — the
visceral-fat 2.7 case now reads as "comfortably in the healthy band"
rather than reciting how many times it was logged.

The block is absent for uncovered metrics and for a sex-split band with
no known sex, so those cards fall back to exactly today's personal-relative
behaviour. Band edges carry no strip risk here — the generic assessment
path runs no post-generation numeric grounding gate.
This commit is contained in:
Marc-André Bombeck committed 2026-07-07 00:36:43 +02:00
1 parent 3ce396081f
commit 52d09d17c7
3 files changed
+52 -2

No files matched your search

@@ -119,6 +119,25 @@ describe("metric-archetype user prompt — context block plumbing", () => {
expect(out).not.toContain("undefined");
});
it("appends the interpretation block when provided (Welle J)", () => {
const out = getMetricArchetypeUserPrompt(
meta,
"{}",
"2026-06-05",
"en",
"",
"",
"INTERPRETATION CONTEXT (guideline bands): the healthy band.",
);
expect(out).toContain("INTERPRETATION CONTEXT");
});
it("omits the interpretation block cleanly when absent", () => {
const out = getMetricArchetypeUserPrompt(meta, "{}", "2026-06-05", "en");
expect(out).not.toContain("INTERPRETATION CONTEXT");
expect(out).not.toContain("undefined");
});
it("makes the closing step conditional in the user instruction (both locales)", () => {
expect(
getMetricArchetypeUserPrompt(meta, "{}", "2026-06-05", "en"),
+13 -2
View File
@@ -127,6 +127,13 @@ export function getMetricArchetypeUserPrompt(
* grounded in already-computed data; optional and may be empty.
*/
assessmentContextBlock?: string,
/**
* v1.27.13 (Welle J) — the guideline interpretation block (band table +
* server-computed position) for metrics the knowledge base covers. Absent
* for metrics with no guideline band — the prompt then stays personal-
* relative, exactly as before.
*/
interpretationBlock?: string,
): string {
const ctxBlock =
previousContextBlock && previousContextBlock.trim().length > 0
@@ -136,14 +143,18 @@ export function getMetricArchetypeUserPrompt(
assessmentContextBlock && assessmentContextBlock.trim().length > 0
? `\n\n${assessmentContextBlock}\n`
: "";
const interpBlock =
interpretationBlock && interpretationBlock.trim().length > 0
? `\n\n${interpretationBlock}\n`
: "";
if (locale === "en") {
return `Date: ${todayKey} (Europe/Berlin)
Write one short assessment of this person's ${meta.displayName.toLowerCase()}: name the current level with a concrete number from the snapshot, place the recent window against their own weekly/monthly baseline, and — when something is genuinely actionable — close with one doable step; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.${ctxBlock}${extraBlock}
Write one short assessment of this person's ${meta.displayName.toLowerCase()}: name the current level with a concrete number from the snapshot, place the recent window against their own weekly/monthly baseline, and — when something is genuinely actionable — close with one doable step; when nothing is, skip the step rather than manufacture filler. Judge confidence from the measurement count and recency.${ctxBlock}${extraBlock}${interpBlock}
${snapshotJson}`;
}
return `Datum: ${todayKey} (Europe/Berlin)
Schreibe eine kurze Einschätzung zu ${meta.displayName} dieser Person: benenne das aktuelle Niveau mit einer konkreten Zahl aus dem Snapshot, ordne das recent-Fenster gegen die eigene Wochen-/Monats-Baseline ein und schließe — wenn etwas wirklich umsetzbar ist — mit einem machbaren Schritt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.${ctxBlock}${extraBlock}
Schreibe eine kurze Einschätzung zu ${meta.displayName} dieser Person: benenne das aktuelle Niveau mit einer konkreten Zahl aus dem Snapshot, ordne das recent-Fenster gegen die eigene Wochen-/Monats-Baseline ein und schließe — wenn etwas wirklich umsetzbar ist — mit einem machbaren Schritt; ist nichts umsetzbar, lass den Schritt weg statt Fülltext zu erfinden. Konfidenz aus Messanzahl und Aktualität ableiten.${ctxBlock}${extraBlock}${interpBlock}
${snapshotJson}`;
}
+20
View File
@@ -26,6 +26,7 @@ import {
getMetricArchetypeSystemPrompt,
getMetricArchetypeUserPrompt,
} from "@/lib/ai/prompts/metric-archetypes";
import { buildInterpretationBlock } from "@/lib/ai/prompts/interpretation-block";
import {
getMetricStatusMeta,
metricStatusScope,
@@ -461,6 +462,24 @@ export async function generateMetricStatus(args: {
locale,
);
// v1.27.13 (Welle J) — the guideline interpretation block for metrics the
// knowledge base covers (visceral fat, resting HR, SpO₂, respiratory rate,
// body temperature, sleep duration, waist, PWV). The band position is
// computed from the signal's current value (or the latest day when no signal
// window exists); absent for uncovered metrics (fail-soft to personal-
// relative). Sex-split bands (waist) resolve only when the profile carries a
// sex.
const interpretationValue = signal?.current ?? latest?.value ?? null;
const interpretationBlock =
interpretationValue !== null
? buildInterpretationBlock({
metricKey: meta.id,
value: interpretationValue,
sex,
locale,
})
: undefined;
const outcome = await runStatusCompletion({
userId: args.userId,
cacheAction,
@@ -473,6 +492,7 @@ export async function generateMetricStatus(args: {
locale,
previousContextBlock,
contextBlock,
interpretationBlock,
),
// v1.12.1 (D1) — the phrasing task benefits from a touch more sampling
// entropy while the FACTS stay pinned by the snapshot + the