feat(insights): metric:MOOD inline charts + evidence-contract prompt

Bucket D phase 1 of the v1.4.3 polish pass — the AI insights felt
"static" because (a) findings repeated phrasing with no concrete
numbers and (b) the inline-chart machinery existed but was prompted
permissively, so the model rarely used it.

Two concrete moves, scoped to ship in this hotfix:

1. **`metric:MOOD` is now an allowlisted chart token.** Renderer in
   `<InsightAdvisorCard>` branches on `tokenKind(token)` — measurement
   tokens still mount `<HealthChart>`, but `metric:MOOD` mounts the
   dedicated, self-fetching `<MoodChart>` instead. The mood timeline
   was previously the most under-served by the AI prose because it
   couldn't be charted inline; now the model can illustrate adherence-
   risk and mood-drift findings with the same chart users see on the
   /mood page. Drift test was updated in lockstep — the v1.4.2 guard
   that asserted MOOD was excluded now asserts it's present, plus a
   new guard asserts COMPLIANCE is still excluded (no self-fetching
   wrapper exists yet for `<ComplianceLineChart>`; ship that in v1.5).

2. **Prompt grew an EVIDENCE CONTRACT and a forbidden-phrase list.**
   The base system prompt (DE + EN both) now requires every
   `findings[]` entry to put a concrete number from the snapshot in
   `value` ("138/85 mmHg", "+0.4 mmol/L vs. 30d-avg") instead of
   adjective prose ("slightly elevated"). Findings without a snapshot
   anchor MUST be omitted — variable cardinality 0..8 explicitly
   stated, sorted by salience, NO padding. The forbidden-phrase list
   blocks generic boilerplate that signals ungrounded output: "achte
   auf ausreichend Schlaf", "drink enough water", "regular exercise",
   "consult your doctor" (except in the disclaimer field which exists
   precisely for that line). The chart-token instruction flipped from
   passive ("Du DARFST") to active ("Bevorzuge Tokens immer dann,
   wenn …") with three explicit triggers — temporal claims, threshold
   references, window comparisons — that reliably surface chart-worthy
   findings.

Carved out of v1.4.3 for v1.5: telescoping multi-resolution payload
(14d daily + 10w weekly + 21mo monthly + yearly summary per QA
research 1) and the evidence-ref output schema extension (QA 2).
Both touch the prompt's input shape and would change every cache
key + token bill — they want their own phase.

Tests + typecheck green (671/671 — one new chart-tokens drift
guard).

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-08 21:41:29 +02:00
1 parent c05f48f520
commit dc01ab7934
4 files changed
+67 -26

No files matched your search

@@ -19,9 +19,11 @@ import {
} from "lucide-react";
import { useTranslations, useFormatters } from "@/lib/i18n/context";
import { HealthChart } from "@/components/charts/health-chart";
import { MoodChart } from "@/components/charts/mood-chart";
import {
parseChartTokens,
stripChartTokens,
tokenKind,
tokenToMetric,
type ChartToken,
} from "@/lib/insights/chart-tokens";
@@ -238,6 +240,7 @@ function InlineCharts({ tokens }: { tokens: ChartToken[] }) {
<div data-slot="insight-inline-charts" className="space-y-3">
{unique.map((token) => {
const metric = tokenToMetric(token);
const kind = tokenKind(token);
const titleKey = INLINE_CHART_TITLE_KEYS[metric];
const title = titleKey ? t(titleKey) : metric;
return (
@@ -246,7 +249,11 @@ function InlineCharts({ tokens }: { tokens: ChartToken[] }) {
data-slot="insight-inline-chart"
data-metric={metric}
>
<HealthChart types={[metric]} title={title} />
{kind === "mood" ? (
<MoodChart />
) : (
<HealthChart types={[metric]} title={title} />
)}
</div>
);
})}