mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 12:22:03 +00:00
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:
1 parent
4d3b12936a
commit
59a4a89b62
4 files changed
+72
-4
No files matched your search
+3
-3
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user