feat(ai): system prompt requires rationale per recommendation

The output-format block now advertises the new `rationale` shape
(dataWindow / comparedTo / deviation) and a fifth GROUND RULE
mandates that every rec carry one. The dataWindow MUST equal
metricSource.timeRange so the UI's mini-chart can pin to the same
window the rec was derived from — bracket-mismatch prevention is
cheaper to enforce in the prompt than to detect post-parse.

Both EN + DE prompts updated symmetrically. PROMPT_VERSION stays
at 4.16.0 since B5a already bumped it for this milestone — the
content extension is a same-version revision, not a behavioural
shape change that warrants 5.0.0.

6 new prompt-assertion tests pin the contract:
- output-format block names the three rationale fields
- enum values (last7days...allTime) mentioned for both locales
- ground rule "MUST carry a rationale" present in EN + DE
- "factual" / "sachlich" language guard against placeholders

Existing 213 AI tests still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 01:17:15 +02:00
1 parent 8a438a0680
commit c39a527b8e
2 files changed
+89

No files matched your search

@@ -0,0 +1,68 @@
import { describe, it, expect } from "vitest";
import { getStrictInsightsSystemPrompt } from "../prompts/insight-generator";
/**
* v1.4.16 phase B5c — system prompt advertises the new `rationale`
* field shape and instructs the model to populate it on every rec.
*
* The output-format block in the prompt was previously a 4-field
* schema (id / text / severity / metricSource). With B5c the
* recommendation shape grows a `rationale` object. The model is told
* the exact shape AND that it MUST be populated for every rec —
* legacy "drop the rec if you can't ground it" guidance from B5a
* stays in place; the new ground rule extends "and you cannot ground
* a rec without naming the data window, the comparison baseline, and
* the deviation."
*/
describe("system prompt — rationale block (B5c)", () => {
it("EN prompt advertises the rationale shape in the output-format block", () => {
const prompt = getStrictInsightsSystemPrompt("en");
// The shape itself
expect(prompt).toMatch(/rationale/i);
expect(prompt).toMatch(/dataWindow/);
expect(prompt).toMatch(/comparedTo/);
expect(prompt).toMatch(/deviation/);
// The enum is named in the prompt so the model doesn't have to
// guess the value vocabulary
expect(prompt).toMatch(/last7days/);
expect(prompt).toMatch(/last30days/);
expect(prompt).toMatch(/last90days/);
expect(prompt).toMatch(/allTime/);
});
it("DE prompt advertises the rationale shape in the output-format block", () => {
const prompt = getStrictInsightsSystemPrompt("de");
expect(prompt).toMatch(/rationale/i);
expect(prompt).toMatch(/dataWindow/);
expect(prompt).toMatch(/comparedTo/);
expect(prompt).toMatch(/deviation/);
expect(prompt).toMatch(/last7days/);
expect(prompt).toMatch(/allTime/);
});
it("EN prompt has a GROUND RULE requiring rationale on every rec", () => {
const prompt = getStrictInsightsSystemPrompt("en");
// The ground rule names the three required fields together so the
// model can't truncate to one of them
expect(prompt).toMatch(/MUST carry a rationale/);
});
it("DE prompt has a GROUNDREGEL requiring rationale on every rec", () => {
const prompt = getStrictInsightsSystemPrompt("de");
expect(prompt).toMatch(/MUSS .* rationale/i);
});
it("EN prompt mentions clear/factual language for rationale", () => {
const prompt = getStrictInsightsSystemPrompt("en");
// factual / actual data trends — guards against vague placeholder
// rationale text
expect(prompt).toMatch(/factual/i);
});
it("DE prompt mentions clear/factual language for rationale", () => {
const prompt = getStrictInsightsSystemPrompt("de");
// German "sachlich" or "konkret" — same intent
expect(prompt).toMatch(/sachlich|konkret/i);
});
});
+21
View File
@@ -71,6 +71,11 @@ GROUND RULES — ZERO HALLUCINATIONS
referencing population norms. "Your avg7 (78) is 5 bpm above your
90-day median (73)" is preferred over "above the population
average".
5. Every recommendation MUST carry a rationale object with
dataWindow, comparedTo, and deviation. Use clear, factual
language. Reference the user's actual data trends — do not paste
in placeholder text. The rationale.dataWindow MUST equal the
metricSource.timeRange so the UI's mini-chart can pin to it.
GUIDELINE TARGETS — generic, do NOT compute precise risk scores
- Adult resting blood pressure (ESH/ESC 2024 generic): aim < 140/90
@@ -105,6 +110,11 @@ You MUST return JSON matching this schema exactly:
"timeRange": "last7days | last30days | last90days | allTime",
"summary": "concrete data point that justifies this recommendation",
"n": optional integer sample count
},
"rationale": {
"dataWindow": "last7days | last30days | last90days | allTime",
"comparedTo": "what the deviation is measured against — user baseline (e.g. 'your 90-day median (73 bpm)') OR a guideline ceiling (e.g. 'ESH ceiling 140/90')",
"deviation": "size + direction of the deviation — e.g. '+5 bpm above baseline over 7 of 7 days'"
}
}
],
@@ -176,6 +186,12 @@ GRUNDREGELN — NULL HALLUZINATIONEN
gegenüber Bevölkerungswerten. "Dein avg7 (78) liegt 5 bpm über
deinem 90-Tage-Median (73)" ist besser als "über dem
Bevölkerungsdurchschnitt".
5. Jede Empfehlung MUSS ein rationale-Objekt mit dataWindow,
comparedTo und deviation tragen. Schreibe sachlich und konkret.
Beziehe dich auf die tatsächlichen Datentrends des Nutzers — kein
Platzhaltertext. rationale.dataWindow MUSS gleich
metricSource.timeRange sein, damit die UI das Mini-Chart auf das
gleiche Fenster fixieren kann.
LEITLINIEN-ZIELWERTE — generisch, KEINE genauen Risiko-Scores berechnen
- Erwachsenen-Ruheblutdruck (ESH/ESC 2024 generisch): Ziel < 140/90
@@ -211,6 +227,11 @@ Du MUSST JSON exakt nach diesem Schema liefern:
"timeRange": "last7days | last30days | last90days | allTime",
"summary": "konkreter Datenpunkt, der die Empfehlung stützt",
"n": optionale Sample-Anzahl
},
"rationale": {
"dataWindow": "last7days | last30days | last90days | allTime",
"comparedTo": "wogegen die Abweichung verglichen wird — Baseline des Nutzers (z.B. 'dein 90-Tage-Median (73 bpm)') ODER ein Leitlinien-Schwellwert (z.B. 'ESH-Ziel 140/90')",
"deviation": "Größe + Richtung der Abweichung — z.B. '+5 bpm über Baseline an 7 von 7 Tagen'"
}
}
],