fix(insights): strip metric tokens in recommendation prose + DE component labels

Two related leak fixes traced to the same root: every other AI-prose
surface ran user-visible text through `stripChartTokens()`, but the
recommendation card's always-visible text was rendered directly. The
model embeds `metric:WEIGHT` and friends in the rec text expecting an
inline mini-chart, but the chart slot lives in the EXPANDED rationale
block — so the token had nowhere to be consumed and leaked verbatim
to the DOM.

  - `<RecommendationCard>` now wraps `norm.text` in
    `stripChartTokens()`, matching the contract every other
    rec / finding / summary surface already uses.
  - DE Health-Score component labels surfaced English nouns ("Mood",
    "Medis", "BD") instead of full German. Rename to "Blutdruck",
    "Stimmung", "Einnahmetreue", "Gewicht" so the four sub-bars read
    natively German. The "Mut" the maintainer reported on prod was
    voice-to-text rendering "Mood".
  - i18n-integrity test pins the four component labels so a copy-paste
    regression can't reintroduce the leak.

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 20:54:21 +02:00
1 parent 4d3b12936a
commit 59a4a89b62
4 files changed
+72 -4

No files matched your search

+3 -3
View File
@@ -774,10 +774,10 @@
"progressAria": "Gesundheitsscore-Fortschritt",
"deltaVsLastWeek": "{delta} im Vergleich zur Vorwoche",
"deltaUnavailable": "Noch keine Historie — schau nächste Woche wieder vorbei.",
"componentBp": "BD",
"componentBp": "Blutdruck",
"componentWeight": "Gewicht",
"componentMood": "Mood",
"componentCompliance": "Medis",
"componentMood": "Stimmung",
"componentCompliance": "Einnahmetreue",
"disclaimer": "Orientierungshilfe — keine ärztliche Bewertung.",
"askCoach": "Coach fragen",
"coachPrompt": "Warum ist mein Gesundheitsscore {score} von 100?"
@@ -243,4 +243,36 @@ describe("<RecommendationCard>", () => {
);
expect(html).not.toContain("data-feedback-thumb=");
});
/**
* v1.4.22 A6 — strip chart-tokens from the always-visible
* recommendation text. W1a §3 traced the production "metric:WEIGHT"
* leak Marc saw at the bottom of the advisor card to this exact
* render path: every other AI-prose surface ran through
* `stripChartTokens()`, the recommendation text didn't. Pin the
* strip so a future copy-paste regression can't reintroduce the
* leak.
*/
it("strips literal chart tokens (metric:WEIGHT et al) from the rec text", () => {
const recWithToken = {
...recBase,
text: "Increase fluids. metric:WEIGHT",
};
const html = render(<RecommendationCard rec={recWithToken} index={0} />);
expect(html).not.toContain("metric:WEIGHT");
expect(html).toContain("Increase fluids.");
});
it("strips lowercase / mixed-case model output from the rec text", () => {
const recWithLowercaseToken = {
...recBase,
text: "Watch the trend. metric:blood_pressure_sys today",
};
const html = render(
<RecommendationCard rec={recWithLowercaseToken} index={0} />,
);
expect(html).not.toContain("metric:blood_pressure_sys");
expect(html).toContain("Watch the trend.");
expect(html).toContain("today");
});
});
@@ -11,6 +11,7 @@ import type {
import { HealthChart } from "@/components/charts/health-chart";
import { MoodChart } from "@/components/charts/mood-chart";
import { getMedicalReferenceById } from "@/lib/ai/medical-references";
import { stripChartTokens } from "@/lib/insights/chart-tokens";
import type { Locale } from "@/lib/i18n/config";
import {
RecommendationFeedback,
@@ -333,7 +334,9 @@ export function RecommendationCard({
{norm.severity}
</Badge>
)}
<p className="text-sm leading-snug">{norm.text}</p>
<p className="text-sm leading-snug">
{stripChartTokens(norm.text)}
</p>
</div>
</div>
</div>
@@ -327,4 +327,37 @@ describe("i18n locale file integrity", () => {
todos.map((s) => ` ${s}`).join("\n"),
).toEqual([]);
});
/**
* v1.4.22 A6 — DE locale must not leak English nouns for the
* Health-Score component contract.
*
* The four `componentBp` / `componentWeight` / `componentMood` /
* `componentCompliance` keys are rendered as the four sub-bar
* labels on the Health Score card. Up to v1.4.21 the DE locale
* shipped `componentMood: "Mood"` (an English noun voice-to-text
* rendered as "Mut" on a quick read). Pin the German values so a
* future copy-paste regression can't reintroduce the leak.
*/
it("DE locale renders Health-Score component labels in German", () => {
const de = JSON.parse(readFileSync(DE_PATH, "utf8")) as Record<
string,
unknown
>;
const flat: [string, string][] = [];
flattenValues(de, "", flat);
const byKey = new Map(flat);
const EXPECTED: Array<[string, string]> = [
["insights.healthScore.componentBp", "Blutdruck"],
["insights.healthScore.componentWeight", "Gewicht"],
["insights.healthScore.componentMood", "Stimmung"],
["insights.healthScore.componentCompliance", "Einnahmetreue"],
];
for (const [key, expectedDe] of EXPECTED) {
const actual = byKey.get(key);
expect(actual, `DE label for ${key}`).toBe(expectedDe);
}
});
});