diff --git a/src/lib/ai/prompts/__tests__/assessment-prompt-contract.test.ts b/src/lib/ai/prompts/__tests__/assessment-prompt-contract.test.ts index ed68da4d4..ade53e8e8 100644 --- a/src/lib/ai/prompts/__tests__/assessment-prompt-contract.test.ts +++ b/src/lib/ai/prompts/__tests__/assessment-prompt-contract.test.ts @@ -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"), diff --git a/src/lib/ai/prompts/metric-archetypes.ts b/src/lib/ai/prompts/metric-archetypes.ts index f03de0c36..902e1d8d7 100644 --- a/src/lib/ai/prompts/metric-archetypes.ts +++ b/src/lib/ai/prompts/metric-archetypes.ts @@ -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}`; } diff --git a/src/lib/insights/metric-status.ts b/src/lib/insights/metric-status.ts index b7fcde718..2ea11308b 100644 --- a/src/lib/insights/metric-status.ts +++ b/src/lib/insights/metric-status.ts @@ -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